上篇博文介绍了Embedding模型与向量数据库及相关的RAG技术,这里介绍另一个应用场景,基于Embedding模型与向量数据库实现一个图文搜索功能,可以根据用户输入的文字搜索其相关的图片。
多模向量模型
既然要搜索图片,当然需要将图片内容进行向量化,这里就使用了阿里百炼平台提供的通用多模态向量模型multimodal-embedding-v1
向量数据库还是选用milvus
本地部署
遇到的问题
既然使用阿里百炼提供的模型,当然集成框架自然会使用spring ai alibaba
,配置好向量模型相关的参数直接注入框架提供的DashScopeEmbeddingModel
,调用call()
传入图片url或Base64编码即可向量化才对,甚至结合MilvusVectorStore
去写入图片搜索图片时,它会自动调用本地的EmbeddingModel
对象向量化,类似上篇博文中的知识库导入、RAG搜索一样,用不了几行代码即可实现。但是实际操作遇到一个奇怪的报错:
{"code":"InvalidParameter","message":"url error, please check url!","request_id":"267f626a-9e80-9ca4-8c14-34d2ad2bf934"}
org.springframework.ai.retry.NonTransientAiException: 400 - {"code":"InvalidParameter","message":"url error, please check url!","request_id":"267f626a-9e80-9ca4-8c14-34d2ad2bf934"}
at org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration$2.handleError(SpringAiRetryAutoConfiguration.java:100) ~[spring-ai-spring-boot-autoconfigure-1.0.0-M5.jar:1.0.0-M5]
at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.StatusHandler.lambda$fromErrorHandler$1(StatusHandler.java:71) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.StatusHandler.handle(StatusHandler.java:146) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.applyStatusHandlers(DefaultRestClient.java:711) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:200) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.readBody(DefaultRestClient.java:698) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.toEntityInternal(DefaultRestClient.java:668) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.toEntity(DefaultRestClient.java:657) ~[spring-web-6.1.16.jar:6.1.16]
at com.alibaba.cloud.ai.dashscope.api.DashScopeApi.embeddings(DashScopeApi.java:285) ~[spring-ai-alibaba-core-1.0.0-M5.1.jar:1.0.0-M5.1]
at com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel.lambda$call$1(DashScopeEmbeddingModel.java:136) ~[spring-ai-alibaba-core-1.0.0-M5.1.jar:1.0.0-M5.1]
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:357) ~[spring-retry-2.0.11.jar:na]
at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:230) ~[spring-retry-2.0.11.jar:na]
at com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel.lambda$call$3(DashScopeEmbeddingModel.java:133) ~[spring-ai-alibaba-core-1.0.0-M5.1.jar:1.0.0-M5.1]
at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.13.10.jar:1.13.10]
at com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel.call(DashScopeEmbeddingModel.java:132) ~[spring-ai-alibaba-core-1.0.0-M5.1.jar:1.0.0-M5.1]
at top.chengpei.ai.picturesearch.controller.PictureSearchController.upload(PictureSearchController.java:38) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.16.jar:6.1.16]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.16.jar:6.1.16]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.34.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.16.jar:6.1.16]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.34.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.16.jar:6.1.16]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.16.jar:6.1.16]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.16.jar:6.1.16]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.16.jar:6.1.16]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
这里的InvalidParameter
肯定是阿里云返回的,在错误信息列表里确实有这个错误码https://help.aliyun.com/zh/model-studio/developer-reference/error-code?spm=0.0.0.i3
但是并没有错误信息为url error, please check url!
的详细说明,这就很奇怪,各种尝试没有解决,最后在github的spring-ai-alibaba项目里找到有人提问类似的报错,得到回复是当前框架并不支持multimodal-embedding-v1
模型。
解决方案
既然调用multimodal-embedding-v1
模型不能使用spring ai alibaba
框架,那么查看官方提供的API调用示例,提供了Python调用与HTTP接口调用两种方式,这里只好使用手写http调用解析的过程去向量化数据了,当然也无法使用MilvusVectorStore
直接写入或者查询数据了,但是可以使用更加底层的MilvusServiceClient
,手动将向量化后的数据写入向量数据库,再手动向量化用户的输入本文,传入向量化的结果到库中查询。
代码演示
具体代码如下,两个接口功能分别是初始化数据以及根据用户输入本文搜索图片
@GetMapping(value = "/init", produces = "application/json")
public String init() {
List<String> picUrlList = new ArrayList<>();
picUrlList.add("https://home.chengpei.top:8443/root/picture/aircraft1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/aircraft2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/bot1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/bot2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/car1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/car2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/car3.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/cat1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/cat2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/cat3.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/desk1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/desk2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/dog1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/dog2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/dog3.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/horse1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/horse2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/house1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/house2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/phone1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/phone2.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/phone3.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/tree1.jpg");
picUrlList.add("https://home.chengpei.top:8443/root/picture/tree2.jpg");
List<String> docIdArray = new ArrayList<>();
List<String> contentArray = new ArrayList<>();
List<JSONObject> metadataArray = new ArrayList<>();
List<List<Float>> embeddingArray = new ArrayList<>();
for (String picUrl : picUrlList) {
docIdArray.add(UUID.randomUUID().toString());
contentArray.add(picUrl);
metadataArray.add(new JSONObject());
embeddingArray.add(EmbeddingUtils.embedding(picUrl, "image"));
}
List<InsertParam.Field> fields = new ArrayList<>();
fields.add(new InsertParam.Field(MilvusVectorStore.DOC_ID_FIELD_NAME, docIdArray));
fields.add(new InsertParam.Field(MilvusVectorStore.CONTENT_FIELD_NAME, contentArray));
fields.add(new InsertParam.Field(MilvusVectorStore.METADATA_FIELD_NAME, metadataArray));
fields.add(new InsertParam.Field(MilvusVectorStore.EMBEDDING_FIELD_NAME, embeddingArray));
InsertParam insertParam = InsertParam.newBuilder()
.withDatabaseName(MilvusVectorStore.DEFAULT_DATABASE_NAME)
.withCollectionName("picture_search")
.withFields(fields)
.build();
this.milvusServiceClient.insert(insertParam);
return "success";
}
@GetMapping(value = "/search", produces = "application/json")
public List<JSONObject> search(String message) {
List<String> outFieldNames = new ArrayList<>();
outFieldNames.add(MilvusVectorStore.DOC_ID_FIELD_NAME);
outFieldNames.add(MilvusVectorStore.CONTENT_FIELD_NAME);
var searchParamBuilder = SearchParam.newBuilder()
.withDatabaseName(MilvusVectorStore.DEFAULT_DATABASE_NAME)
.withCollectionName("picture_search")
.withTopK(5)
.withOutFields(outFieldNames)
.withVectors(List.of(EmbeddingUtils.embedding(message, "text")))
.withVectorFieldName(MilvusVectorStore.EMBEDDING_FIELD_NAME);
R<SearchResults> searchResults = milvusServiceClient.search(searchParamBuilder.build());
SearchResults resultsData = searchResults.getData();
System.out.println(resultsData.getResults());
List<JSONObject> result = new ArrayList<>();
SearchResultData results = resultsData.getResults();
for (int i = 0; i < results.getTopK(); i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("docId", results.getFieldsData(0).getScalars().getStringData().getData(i));
jsonObject.put("content", results.getFieldsData(1).getScalars().getStringData().getData(i));
jsonObject.put("scores", results.getScores(i));
result.add(jsonObject);
}
return result;
}
图片准备了24张不同类似的图片,有猫、狗、汽车、飞机、树等,向量数据库创建了一个collection
存储图片的向量数据以及URL
这里需要注意的是向量字段使用维度为1024,跟上一篇博文里的是不一样的,因为根据阿里百炼里multimodal-embedding-v1
模型的描述,这个模型的向量维度就是1024,所以创建这个collection
时维度一定要一致。
调用接口搜索结果如下:
这里因为TopK为5,所以搜索出来的是5条数据,并且前三条的scores
较高。
具体代码放到了github:
https://github.com/chengpei/spring-ai-demo
模块picture-search-demo
为图片搜索相关代码
评论区