ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

不调 Python,不装向量库:我用纯 Java 写了一套以图搜图引擎

不调 Python,不装向量库:我用纯 Java 写了一套以图搜图引擎 上个月收尾一个 Java 后端的项目客户临时加了个需求商品库想支持拍图找同款。我第一反应是这块得用 Python 吧。然后我停了一下。这个得字是从哪来的一、一个被默认接受的假设做 Java 后端的人大概都有这个条件反射一碰到 AI 需求脑子里自动弹出一句这块得走 Python。图像识别、推荐、风控好像都默认归 Python 管。这个反射不是没道理。模型训练确实得在 Python 里做PyTorch、PaddlePaddle、还有现在的大模型生态训练端的好东西几乎都在那边。这部分我不打算抬杠也没得抬。但问题是我们平时接到的需求绝大多数根本用不上训练。你要做以图搜图需要的是部署一件事把一张图丢进去从图库里找出最像的几张。这件事真正吃的是并发、延迟、稳定性还有跟现有业务系统对接的顺畅程度。而这些恰恰是 JVM 的主场。我们用训练端的短板去否定了部署端的长处。这是我停下来的原因。二、两条老路各自的代价Java 项目里想加 AI 能力通常只有两个选择。第一条路起一个 Python 微服务用 HTTP 或者 gRPC 去调。看着挺直接但你得接受它带来的一整套东西跨语言序列化的开销、两套技术栈、两套部署脚本、两套监控告警。出了线上问题Java 组和算法组得一起排查光是定位在哪一侧就要花掉小半天。系统复杂度不是加了 1是乘了个系数。第二条路干脆把业务搬到 Python 去。这个代价更大等于放弃你整个团队积累的技术栈和基建。我不太甘心就这两个选项。于是花了挺长时间试着走第三条路把 AI 能力整个装进一个 Java 进程里不跨语言不引外部服务。做出来的东西叫ImageSearch4J现在版本 1.0.0已经开源了。POST /api/search │ image (File) topk threshold ▼ 主体检测PicoDet-LCNet, 640×640 ▼ SANMS 精修解决大框吞小框 ▼ 逐候选裁剪 → 特征提取PP-LCNetV2, 224×224 → 512 维 ▼ 逐候选向量检索 → 二次排序定位最佳主体 ▼ 以最佳主体的向量拉出相似图列表整条链路从图片进来到结果出去一个 JAR一个进程。没有 Python没有 RPC也没有一个单独要维护的向量数据库。下面我把里面几个关键的选择讲一讲。这些思考过程比代码本身更值钱也更值得一读。三、三块拼图其实是同一件事要让这条路走通得解决三个问题模型怎么带过来用什么跑推理向量放到哪里检索。对应的就是 ONNX、DJL、Lucene 三块拼图。模型怎么带过来选 ONNX是想让训练和部署彻底分开ONNX 这个格式很多人把它当成一个中间产物导出来就完事。我更愿意把它看成一份契约。它现在是 Linux Foundation 下面的项目不属于 PyTorch也不属于 PaddlePaddle任何一家都没法单方面改它。它定义的是一套跟框架无关的计算图。这意味着什么意味着训练团队用 PyTorch 还是 Paddle、明天想不想换个新框架是他们的自由Java 这边一行代码都不用动。反过来部署侧只要认 ONNX 就行不用管模型是怎么训出来的。两边的交接界面从框架对框架降到了框架对标准。后来我在 ONNX 官网读到一段话几乎是把我这套想法原样说了一遍We believe there is a need for greater interoperability in the AI tools community. Many people are working on great tools, but developers are often locked in to one framework or ecosystem. ONNX is the first step in enabling more of these tools to work together by allowing them to share models.Our goal is to make it possible for developers to use the right combinations of tools for their project. We want everyone to be able to take AI from research to reality as quickly as possible without artificial friction from toolchains.without artificial friction from toolchains人为的工具链摩擦。跨语言调用、多起一套服务、两套部署这些都不是问题本身的难度是工具链自己造出来的难度。举个具体的ImageSearch4J 用的两个模型就是 PP-ShiTu 体系里的 PicoDet-LCNet 和 PP-LCNetV2在 Paddle 侧训练好转成 ONNX 拿过来用。整个 Java 侧只负责推理不碰训练。换成别的模型只要它支持导出 ONNX就能接进来。这个设计还有个不那么显眼的好处。ONNX 的规范里写死了向后兼容新版本的运行时必须能跑旧版本的模型。对打算长期维护的项目来说这一点比现在好不好用重要得多。用什么跑推理选 DJL是因为这件事早就有人做过有人问过Java 做 AI 推理是不是自己写一套很折腾。不用。AWS 有个开源项目叫 Deep Java Library简称 DJL2019 年就在 re:Invent 上开源了。它的定位是 engine-agnostic同一套 Java API底层可以挂 ONNX Runtime、PyTorch、TensorFlow 各种引擎。它在 AWS 自己的云上跑了很久SageMaker 上的 DJL Serving 就是官方的模型部署方案动态批处理、自动扩缩、多引擎托管这些能力都有。说白了Java 跑 AI 推理这件事早就不是没人走的路了。我不用自己去造推理引擎站在 DJL 上面就行精力集中在以图搜图这一件事上。顺带提一个跟安全有关的点。AWS 之前发过一个 DJL 的漏洞公告影响 0.13.0 到 0.36.0 的版本修复线是 0.37.0。本项目用的 0.38.0已经在修复线之上。向量放哪选 Lucene是想把一整个组件消掉这一块我考虑的时间最长。常规做法是上 Milvus、Qdrant 这类专门的向量数据库或者干脆用 Elasticsearch 的 KNN。它们都很强功能也全。但放到我这个场景里它们都有一个共同的问题要多一个东西。多一个进程多一套部署多一份监控多一个会出故障的环节。为了一个检索功能把整个系统的运维面撑大一圈我觉得不划算。Apache Lucene 是另一条路。它是 Java 生态里最经得起考验的搜索内核Elasticsearch 和 Solr 的检索能力其实都是建在它上面的。对我要做的事它有两个特别合适的点第一它是嵌入式的。就是一个 JAR 包跟业务同一个进程没有独立服务没有网络往返。这一整层运维直接没了。第二它从 9.0 开始原生支持 HNSW 向量索引。我在上面又加了标量量化把 float32 压成 int8索引内存大概降到原来的四分之一召回率还能保持在 95% 以上。这部分优化算是白拿的。还有一个附带的好处是我后来才意识到的Lucene 的 Document 既能挂向量字段也能挂文本倒排字段。也就是说向量检索 关键词检索的混合检索这个项目是天然具备架构基础的不需要再引入任何组件。这一点我在后面还会提到。三块拼图讲完其实它们在做同一件事把必须绑在一起的东西压到最少把可以各自独立的部分放到最大。训练和部署解耦模型和引擎解耦AI 能力和基础设施解耦。这也是我后来复盘时才发现的一条线三个选择看着独立底层是同一个原则。四、踩得最深的一个坑大框吞小框讲这块之前先说一句这个算法不复杂前后也就几十行。但它是整个项目里我花时间最多的地方因为找到该改什么这一步比改本身难太多了。事情是这样的图库里有一瓶可乐。我把一张可乐的照片丢进去期待它匹配到那瓶可乐。结果它匹配到了别的东西。准确地说它匹配到了另一张标签特写的图。我一开始以为是模型不准。换了几张图现象很稳定只要目标上有大面积的标签、logo、或者文字匹配就容易跑到局部特写上去。搜可乐给你返回标签图搜饮料瓶给你返回瓶盖图。这就不是偶发了是有一个固定的失败模式在里头。排查的过程我先做的是把主体检测的结果打出来看。一看就明白了。检测器对那张可乐照片输出了两个框一个大框框住整个瓶身置信度 0.60一个小框只框住红白标签那一片置信度 0.95两个框一个对应整瓶一个对应标签。而下游是按置信度排序的0.95 的标签框赢了系统就拿着标签去图库里比对返回的自然全是标签图。问题不在搜得准不准在拿什么去搜这一步就错了。我试过、但都没走通的路搞清楚现象之后我陆续试了几种办法都不行。调置信度阈值。把阈值调高到 0.6 以上标签框是滤掉了但整瓶那个大框0.60也一起没了一个候选都不剩。调低呢噪声更多。这条路走不通因为大框天然分数就低小框天然分数就高两边卡在一个阈值上没法分开。调 NMS 的 IoU 阈值。这个思路是让两个框合并成一个。但 NMS 判断两个框是不是同一个目标用的就是 IoU而大框套小框这种情况重叠面积相对于并集来说小得可怜算下来 IoU 可能只有 0.01。IoU 本来就极小我在它上面怎么调阈值都碰不到这两个框。判据本身就用错了地方。干脆把小框全删掉只留最大的。这个我试的时间最长。它确实能把整瓶留下来但有个副作用小框那个 0.95 的高分也一起被扔掉了。留下的整瓶框还是 0.60在一堆候选里排不到前面。等于用一个问题换来了另一个问题。而且取最大框这个规则本身也站不住。最大那个框不一定是主体它可能框的是整层货架、整张桌面。真实图片里背景框往往比主体框还大。想通的那一刻把上面几条路都堵死之后我才意识到问题出在哪。NMS 的判据是重叠它回答的是这两个框重叠得多不多。但大框套小框这种关系本质是包含不是重叠。这两种关系在几何上完全是两码事包含关系的 IoU 可以趋近于 0重叠关系的 IoU 一定很大。用重叠这把尺子去量包含量不出来再怎么调都没用。那就换一把尺子。不看重叠面积而是直接比较坐标判断一个框是不是几何上完全落在另一个框里面。想通这点之后剩下的就是把它写出来。我给它起名叫 SANMSStructure-Aware NMS结构感知的非极大值抑制。具体怎么做逻辑就三步按面积从大到小处理所有的框让整体框先出场用坐标直接比较判断两个框是不是几何包含关系注意不是算 IoU如果大框把小框整个包住了就把小框去掉同时把小框的高分继承给大框第三步是我最满意的地方也是它和取最大框拉开差距的地方。它让大框同时拿到了两样东西整体的尺度加上局部的高置信度。整瓶那个框吞掉标签框之后分数从 0.60 变成 0.95排序上自然就赢了。精度和召回在这里不用二选一。核心代码其实就几行// 按面积降序处理 Arrays.sort(sortedIdx, (a, b) - Float.compare(areas[b], areas[a])); // 大框完全包含小框 → 吞并并把小框的高分继承给大框 if (kbox[0] curBox[0] EPS kbox[1] curBox[1] EPS kbox[2] curBox[2] - EPS kbox[3] curBox[3] - EPS) { // ★ 分数继承点对点取最大值发生在吞并瞬间 if (scores[curIdx] scores[kidx]) { scores[kidx] scores[curIdx]; } isContained true; }它和传统 NMS 是什么关系这里要澄清一个容易误解的地方SANMS 不是拿来替代 NMS 的它是补在 NMS 后面的。PP-ShiTu 体系的检测模型是端到端的检测头输出里其实已经带了一轮模型内置的 NMS。所以我的处理不是去动它而是在它之后再叠一层精修。两者管的事情不一样内置的 NMS 管重叠两个框 IoU 很大大概率是同一个目标去重。SANMS 补包含两个框 IoU 极小但其实是同一个目标合并。一个兜常规情况一个补特殊死角配合起来层层收敛。实际代码里就是先按分数粗筛出一批候选再交给 SANMS 精修形成一个粗筛保召回、精修保精度的闭环。它有个我很喜欢的性质零额外参数零训练成本。它是一段纯几何后处理不需要重新训练检测器也不引入任何需要调的参数。你可以直接把它叠在任何目标检测模型的输出后面不用担心这个数据集上要调成多少。这一点对我很重要。因为这意味着它是一个纯粹的算法改进不会给用的人增加任何调参负担。为什么值得单拎出来说回到开头那个场景问题的本质其实不是检索得准不准而是系统拿着什么东西去检索。传统 NMS 会让整瓶和标签两个框都留下来高分的小框胜出于是系统拿着标签去搜用户想要的是整瓶拿到的却是标签。这类错从第一步就注定了后面再怎么优化相似度算法都救不回来。而大框吞小框不是个边角情况。商品搜索、服装搜索、地标检索只要目标上有显著的局部纹理这个失败模式就会出现。我在这上面踩过的坑做同类需求的人大概率也会踩一遍。SANMS 想解决的就是这一件事让检测到了什么和该拿什么去搜重新对齐。五、免训练这件事值得单独说说免训练是这个项目一个挺重要的特性但容易被当成宣传词我想把它讲透。为什么以图搜图能免训练根子在一个经常被混淆的区别上它是检索任务不是分类任务。分类任务检索任务以图搜图知识存在哪模型的权重里图库里新增一个类别重新训练要数据要算力加几张图就行模型的角色分类器一个通用的「图 → 向量」函数分类任务里知识是烧进模型权重里的你要认出一个新类别就得重新训练。但检索任务不一样知识在图库里不在模型里。模型只需要干一件事把任意一张图稳定地变成一个向量。这件事一个通用的预训练模型就能做得很好不需要针对你的数据做任何调整。这就是免训练成立的原因。它带来几个很实际的结果。新品类上线等于往图库加图。不用训练、不用标注、不用请算法工程师介入。在这个架构里向量更新这个操作替代的正是别的系统里重新训练的那一步。用的人不需要懂 AI。不用知道什么是损失函数、学习率、batch size也不用准备训练用的 GPU 集群。你只需要准备好图片。Java 侧天然只有推理。训练是重活数据集、分布式、调参、GPU 调度。推理是轻活加载模型跑一次前向。正因为是免训练训练那一整坨复杂度直接被消掉了。前面说这个项目能保持轻量根子就在这里。这也是它和 PP-ShiTu 一脉相承的地方同一套底层模型同样的定位同样免训练。你要换个行业场景通常需要的只是换一个新的图库。六、它现在长什么样说了这么多想法看看实际的东西。架构图┌───────────────────────────────────────────────────────────────────────┐ │ Spring Boot 3 应用单进程 │ │ │ │ POST /api/search │ │ │ image (File) · topk · threshold │ │ ▼ │ │ ┌─────────────────── ImageSearchService ──────────────────┐ │ │ │ Async(aiInferExecutor) │ │ │ │ │ │ │ │ ① PipelineService.process() —— 定位「最佳主体」 │ │ │ │ ├─ MainBodyDetectionService.predict() │ │ │ │ │ PicoDet-LCNet 检测 → Top5 粗筛 → 阈值过滤 │ │ │ │ │ → SANMS 精修大框吞小框 分数继承 │ │ │ │ └─ 逐候选裁剪子图 → PP-LCNetV2 提特征 → 向量检索 │ │ │ │ 二次排序同分取面积大者 → 确定为 match │ │ │ │ │ │ │ │ ② 以 match 的向量检索出完整相似列表 similarList │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 推理层AWS DJL 0.38 ── ONNX Runtime 1.29 ── PicoDet / PP-LCNetV2 │ │ 检索层Apache Lucene 9.12 ── HNSW 标量量化 ── 本地索引目录 │ └───────────────────────────────────────────────────────────────────────┘① 主体检测PicoDet-LCNet输入图片检测出图中所有潜在主体框随后经过一条漏斗式的处理链模型原始输出 → Top5 粗筛 → 阈值过滤 → SANMS 精修 保召回 去噪声 去包含冗余② 特征提取PP-LCNetV2对每个候选框裁剪出子图缩放到 224×224、按 ImageNet 均值方差归一化后提取512 维特征向量已 L2 归一化。③ 向量检索Lucene HNSW每个候选子图分别检索逐候选打分、再二次排序——同等分数取面积更大者最终确定「最佳主体」。随后用它对应的向量拉出完整的相似度列表。链路与接口对外就一个主要接口POST /api/search Content-Type: multipart/form-data参数类型必填默认值说明imageFile是—待检索的图片topkint否10返回结果数量thresholdfloat否0.4向量检索的相似度阈值一个 curl 例子curl -X POST http://localhost:8080/api/search \ -F image./cola.jpg \ -F topk10 \ -F threshold0.4返回结构长这样真实抓包{ state: 1, code: 200, message: Success, data: { match: { name: 康师傅冰红茶, path: 142/2.jpg, md5: 96b9f53217b116af6b767ba033e74b9f, score: 0.8343468 }, similarList: [ ... ], candis: [ ... ], rect: [499.86, 9.44, 801.02, 945.43] } }这里有两个字段值得说一下。match是系统认定的最佳匹配similarList是按分数排好的完整相似列表。rect是最佳匹配主体的矩形框candis是除它以外的其它候选框都带检测置信度。为什么要拆成两组因为match回答的是这张图里最可能是什么similarList回答的是图库里有哪些像的这两件事语义不一样前端可以分开展示。而rect和candis分开是为了让用户在图上直接看出来系统为什么这么判断。把认定主体和落选候选用两种颜色画出来看一眼就懂。顺便说一个细节就算向量库里一个都没匹配上rect和candis也照样有值。意思是前端可以提示检测到目标了但库里没有匹配而不是给用户一个空白页。界面项目里带了两个零框架的静态页面没上任何前端框架纯 HTML 原生 JS。搜索页支持三种传图方式本地上传、图片链接、粘贴截图。这三种方式在提交前都会被统一转成File对象后端只认一个image字段接口不用做任何适配。结果回来后预览图上会自动标出最佳匹配框绿色实线和候选框橙色虚线下面是匹配卡片和相似图列表。另一个是向量更新的入口页。主要用来展示向量库增量的交互形态批量上传、统计、日志这些。真要接后端做生产级的向量管理它的接口还需要补齐。我在文档里也是这么标注的没打算把它包装成完整后台。七、有些话README 里不太方便写这部分是我写 README 时删掉、但觉得值得单独讲的内容。它和 Spring AI是同一个位置我一直在找一个类比帮别人理解这个项目到底是什么。后来想清楚了ImageSearch4J 在 Java 图像检索里的位置相当于 Spring AI 在 Java LLM 里的位置。这话得说准确一点。Spring AI 自己不提供大模型它做的是把大模型能力接进 Spring 生态让开发者用熟悉的方式调用。我这个项目也一样模型不是我的推理引擎不是我的检索引擎也不是我的。我做的是把它们接起来并且接成一个 Java 开发者顺手就能用的形态。区别在于抽象粒度Spring AI 抽象的是怎么调用大模型面向多家供应商我抽象的是一整条图像检索链路怎么跑面向业务开发者。同一层同一类使命形态不完全一样。说实在的这个类比我没放进 README。刚开源的项目上来就说我相当于某某容易被读成自抬身价。但在这个场合讲我觉得它是个挺好用的坐标。定位是轻量底座而且不打算变这个项目的定位是轻量级底座我打算一直守着它。原因很简单庞然大物天生吓退人。一个新人点进来看到几十个模块、一屏配置项第一反应是关掉。而一个底座真正的价值恰恰是让人愿意用它、用得上手。所以我给自己定了个判断标准挺土的但好用这个改动会不会让从 clone 到搜出第一张图的步骤变多会那它就不该进主干再合理也先记下来。功能多和门槛低很多时候是打架的。我尽量选后者。将来这个项目如果真的有一天需要变大比如要支持大规模图库、要集群、要做多租户那就另起一个新项目。旧项目不必背新项目的包袱新项目也不必迁就旧项目的克制。这是对两边都好的做法。有些没做是刻意的举几个例子。主体检测的阈值我是硬编码的没暴露成配置。因为它是算法内部参数交给二次开发的人去操心不合适。对外只留一个threshold管向量检索的精度就够了。能配的参数越多用的人要做的决定就越多上手成本就越高。混合检索也是。前面说了Lucene 让向量 关键词的混合检索在这个项目里几乎零成本把name也写进文本字段查询时用BooleanQuery把 KNN 子查询和词项子查询组合起来融合就发生在同一次查询内部。但我没有把它放进默认路径。因为新用户不该在跑通第一张图之前就被迫去理解两路检索怎么融合。能力留在架构里要不要开留给有经验的人了。这两件事都属于同一类能做但默认不做。不是能力缺失是刻意把复杂度停在该停的地方。八、它不解决什么一个项目只讲能做什么不太可信。我说说它现在做不了什么。它不是拿来训练的。前面反复讲过它只管推理和检索那一侧。你要自己训练模型、做微调那还得回 Python。它和 Elasticsearch 是两种取舍。如果你公司已经有一套 ES 集群那 ES 8.x 自带的向量能力加上你现有的基建可能是更省事的选择。我这个项目走的是嵌入式路线胜在独立、轻、不用额外运维但这两条路的取舍不一样不存在谁绝对更好。混合检索默认是关的。想要的话得自己接。我觉得把这些提前说清楚比让人用了才发现要好。边界清楚的项目用起来才踏实。九、快速上手前面讲的都是为什么这一节讲怎么做。按下面的步骤走从零到搜出第一张图大概十来分钟。环境要求Zulu JDK 17 或以上Maven 3.8 或以上就这两条不需要 Python也不需要单独装数据库。第一步准备图库项目本身不打包图库数据你需要自己准备一份。这里推荐用 PP-ShiTu 官方的示例数据集drink_dataset_v2.0里面是各种饮料的图片正好适合演示。# 下载 wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/data/drink_dataset_v2.0.tar # 解压 tar -xvf drink_dataset_v2.0.tar # 重命名为 image_gallery mv drink_dataset_v2.0 image_gallery # 移动到用户主目录 mv image_gallery ~/图库最终要放在用户主目录下也就是~/image_gallery/。放好之后目录结构是这样~/image_gallery/ ├── gallery/ # 图库本体 │ ├── 142/ # 按分类编号分的子目录 │ ├── 164/ │ ├── ... │ └── drink_label_all.txt # 标签文件 ├── test_images/ # 官方给的测试图可以直接拿来试搜 └── vector_index/ # 索引目录首次启动后自动生成一开始没有里面那个drink_label_all.txt是标签清单每一行是一张图对应一个分类名用制表符分隔。建索引的时候程序就是靠它知道哪张图叫什么名字。如果你放错了位置会怎样不用担心找不到北——应用启动时会检查这个目录发现图库缺失会直接把下载地址打印到日志里然后自己退出不会甩一堆异常堆栈让你猜。第二步启动git clone https://gitee.com/tommycloud/ImageSearch4J.git cd ImageSearch4J mvn spring-boot:run第一次启动会多做两件事都是自动的一是释放模型。两个 ONNX 模型是随包内置的放在项目的resources/models/下picodet_lcnet_x2_5_640_mainbody.onnx主体检测约 28 MBgeneral_PPLCNetV2.onnx特征提取约 18 MB首次启动时程序会把它们复制到~/models/目录。之后启动就直接用不会重复复制。你不用手动下载任何模型文件。二是建索引。如果~/image_gallery/vector_index/是空的程序会自动扫描图库、逐张提取特征、把向量写进索引。这一步是 CPU 密集的图库大的话会花点时间控制台有进度条跑完就完事。顺带说一句索引建好之后日常新增图片会通过 NRT近实时机制刷新后台每秒刷新一次可见性每 5 分钟落一次盘。所以通过接口新增的图不用重启就能被搜到。第三步打开浏览器默认端口是 80http://localhost/index.html在 Linux 或 macOS 下80 是特权端口普通用户绑不上。这时候换个端口启动mvn spring-boot:run -Dspring-boot.run.arguments--server.port8080然后访问http://localhost:8080/index.html就行。页面上传一张饮料的照片就能看到检测框和匹配结果了。手边没合适图片的话~/image_gallery/test_images/里有官方准备好的测试图随便挑一张。第四步调接口界面之外直接调接口也一样。假设端口是 8080curl -X POST http://localhost:8080/api/search \ -F image~/image_gallery/test_images/xxx.jpg \ -F topk10 \ -F threshold0.4返回的就是前面第六节那个结构。想自己写前端的话对着这个结构解析就行。关于onnx-engine-path一个容易让人犯迷糊的配置配置里有一项ty.onnx-engine-path默认值是ty: onnx-engine-path: ${user.home}/.djl.ai/onnx/win-x64先说结论这个参数不是必须的不设置程序照样跑。但生产环境建议设上。下面把它是干嘛的讲清楚。ONNX Runtime 每次被加载的时候都会把它自带的原生库解压出来。在 Windows 上就是三个文件onnxruntime.dll、onnxruntime4j_jni.dll、onnxruntime_providers_shared.dll。默认情况下它解压到一个临时目录等程序结束时再清掉。问题出在清理那一步——它走的是File.deleteOnExit而这个方法只能删空目录。于是每跑一次临时目录里就多留一堆东西跑久了磁盘上会攒一堆垃圾。显式指定一个固定的原生库目录效果就是第一次解压到那儿以后每次直接拿来用不再反复解压也不留垃圾。这就是这个参数存在的理由。现在说那个绕不开的矛盾点。你第一次看到这行配置大概率会懵这个目录里的东西是哪来的我从来没创建过啊。答案有点绕它来自onnxruntime.jar。具体来说是 Maven 依赖里那个com.microsoft.onnxruntime:onnxruntime本项目是 1.29.0 版。这个 jar 里按平台把各家原生库都打包好了路径是ai/onnxruntime/native/平台/各平台对应的目录和文件是这样的平台jar 内目录文件Windows x64ai/onnxruntime/native/win-x64/onnxruntime.dll、onnxruntime4j_jni.dll、onnxruntime_providers_shared.dllLinux x64ai/onnxruntime/native/linux-x64/libonnxruntime.so、libonnxruntime4j_jni.soLinux aarch64ai/onnxruntime/native/linux-aarch64/libonnxruntime.so、libonnxruntime4j_jni.somacOS (Apple Silicon)ai/onnxruntime/native/osx-aarch64/libonnxruntime.dylib、libonnxruntime4j_jni.dylib所以如果你想指定这个参数需要自己动手把对应平台的原生库从 jar 里复制出来放到你指定的目录再把配置指过去。jar 在本地 Maven 仓库里路径是~/.m2/repository/com/microsoft/onnxruntime/onnxruntime/1.29.0/。以 Linux x64 为例一条命令就能解出来unzip -j ~/.m2/repository/com/microsoft/onnxruntime/onnxruntime/1.29.0/onnxruntime-1.29.0.jar \ ai/onnxruntime/native/linux-x64/* \ -d $HOME/.djl.ai/onnx/linux-x64Windows 就把中间的linux-x64换成win-x64以此类推。解完之后把配置改成对应目录ty: onnx-engine-path: ${user.home}/.djl.ai/onnx/linux-x64为什么默认配置写的是win-x64因为我平时开发调试的机器是 Windows就按本机配了。这算不上通用配置你部署到别的平台时记得改成本机对应的目录。不改也没关系程序会退回默认行为该跑还是跑只是会走临时目录那一套。其它可以留意的地方再补几个上手时可能用得上的点配置文件在src/main/resources/application.yml。端口、模型目录、索引目录、线程池大小这些都在里面变量名挺直白的。想换成自己的图库只要准备一份同样结构的目录就行gallery/放图片可以分子目录gallery/drink_label_all.txt写标签每行图片相对路径Tab分类名。换成自己的图库后记得把旧的vector_index/删掉让它重新建。图片能直接在页面上显示是因为静态资源里挂了一个file:${user.home}/image_gallery/的映射。所以图库目录本身同时也是图片的访问根目录。十、最后这个项目做出来最想说的其实是多一种选择。Java 开发者碰到 AI 需求不一定非得转 Python也不一定非得架一个 Python 微服务。这条路现在走得通了模型用 ONNX 中立地承载推理交给 DJL检索交给 LuceneSpring Boot 负责装配。说到底就是各用各的长处谁也不用迁就谁。我一直觉得AI 侧的事情最理想的状态是业务开发者根本感觉不到它的存在。底层那些模型加载、对象池、索引刷新、并发调度都收进 Service 层里业务代码只面对一个干净的方法调用Service RequiredArgsConstructor public class ProductSearchService { private final ImageSearchService imageSearchService; public SearchResult search(byte[] imageBytes) throws Exception { BufferedImage image ImageIO.read(new ByteArrayInputStream(imageBytes)); return imageSearchService.search(image, 10, 0.4f).get(); } }留意一下search标了Async(aiInferExecutor)返回的是CompletableFuture取结果记得.get()或.join()。把 AI 收进底座把业务留给写业务的人。这是这个项目想做的事。项目已经开源Apache License 2.0商用、二开都没问题。如果你正好也在为 Java 生态缺一块 AI 工具而别扭欢迎来看看也欢迎提 issue 和 PR。用得上就点个 star用不上也没关系——这种东西多一个人知道 Java 也能做就多一分价值。项目地址https://gitee.com/tommycloud/ImageSearch4J
返回列表