如何在Ollama和Spring AI上使用本地AI/LLM查詢圖像數(shù)據(jù)庫
譯文譯者 | 李睿
審校 | 重樓
AIDocumentLibraryChat項目的功能已經(jīng)擴(kuò)展到可以查詢圖像的圖像數(shù)據(jù)庫。它使用了可以分析圖像的Ollama的LLava模型。圖像搜索功能利用PostgreSQL的PGVector擴(kuò)展來處理嵌入(Embeddings)。
架構(gòu)
AIDocumentLibraryChat項目的架構(gòu)見下圖:
Angular前端向用戶展示上傳和提問功能。Spring AI后端可以調(diào)整模型的圖像大小,使用數(shù)據(jù)庫存儲數(shù)據(jù)/向量,并使用Ollama的LLava模型創(chuàng)建圖像描述。
圖像上傳/分析/存儲流程見下圖:
圖像通過前端上傳。后端將其調(diào)整為LLava模型可以處理的格式。然后,LLava模型根據(jù)提供的提示生成圖像的描述。調(diào)整之后的圖像和元數(shù)據(jù)存儲在PostgreSQL的關(guān)系表中。然后使用圖像描述來創(chuàng)建嵌入。嵌入與元數(shù)據(jù)一起存儲在PGVector數(shù)據(jù)庫中的描述中,以在PostgreSQL表中找到相應(yīng)的行。然后在前端顯示圖像描述和調(diào)整后的圖像。
圖像查詢的流程見下圖:
用戶可以在前端輸入問題。后端將問題轉(zhuǎn)換為嵌入,并在PGVector數(shù)據(jù)庫中搜索最近的條目。該條目包含圖像表中包含圖像和元數(shù)據(jù)的行ID。然后,將圖像表數(shù)據(jù)與描述一起查詢并顯示給用戶。
后端
為了運行PGVector數(shù)據(jù)庫和Ollama框架,runPostgresql.sh和runOllama.sh文件中包含Docker命令。
后端需要application-ollama.properties文件配置以下條目:
Properties files
# image processing
spring.ai.ollama.chat.model=llava:34b-v1.6-q6_K
spring.ai.ollama.chat.options.num-thread=8
spring.ai.ollama.chat.options.keep_alive=1s
該應(yīng)用程序需要使用Ollama支持(屬性:‘useOllama’)構(gòu)建,并使用‘Ollama’配置文件啟動,需要激活這些屬性以啟用LLava模型并設(shè)置有用的keep_alive。只有當(dāng)Ollama沒有自動選擇正確的線程數(shù)量時,才需要設(shè)置num_thread。
控制器(Controller)
ImageController包含以下端點:
Java
@RestController
@RequestMapping("rest/image")
public class ImageController {
...
@PostMapping("/query")
public List<ImageDto> postImageQuery(@RequestParam("query") String
query,@RequestParam("type") String type) {
var result = this.imageService.queryImage(query);
return result;
}
@PostMapping("/import")
public ImageDto postImportImage(@RequestParam("query") String query,
@RequestParam("type") String type,
@RequestParam("file") MultipartFile imageQuery) {
var result =
this.imageService.importImage(this.imageMapper.map(imageQuery, query),
this.imageMapper.map(imageQuery));
return result;
}
}
查詢端點包含‘postImageQuery(…)’方法,該方法接收包含查詢和圖像類型的表單,并調(diào)用ImageService來處理請求。
導(dǎo)入端點包含‘postImportImage(…)’方法,該方法接收帶有查詢(提示)、圖像類型和文件的表單。ImageMapper將表單轉(zhuǎn)換為ImageQueryDto和Image實體,并調(diào)用ImageService來處理請求。
服務(wù)
調(diào)用ImageService如下:
Java
@Service
@Transactional
public class ImageService {
...
public ImageDto importImage(ImageQueryDto imageDto, Image image) {
var resultData = this.createAIResult(imageDto);
image.setImageContent(resultData.imageQueryDto().getImageContent());
var myImage = this.imageRepository.save(image);
var aiDocument = new Document(resultData.answer());
aiDocument.getMetadata().put(MetaData.ID, myImage.getId().toString());
aiDocument.getMetadata().put(MetaData.DATATYPE,
MetaData.DataType.IMAGE.toString());
this.documentVsRepository.add(List.of(aiDocument));
return new ImageDto(resultData.answer(),
Base64.getEncoder().encodeToString(resultData.imageQueryDto()
.getImageContent()), resultData.imageQueryDto().getImageType());
}
public List<ImageDto> queryImage(String imageQuery) {
var aiDocuments = this.documentVsRepository.retrieve(imageQuery,
MetaData.DataType.IMAGE, this.resultSize.intValue())
.stream().filter(myDoc -> myDoc.getMetadata()
.get(MetaData.DATATYPE).equals(DataType.IMAGE.toString()))
.sorted((myDocA, myDocB) ->
((Float) myDocA.getMetadata().get(MetaData.DISTANCE))
.compareTo(((Float) myDocB.getMetadata().get(MetaData.DISTANCE))))
.toList();
var imageMap = this.imageRepository.findAllById(
aiDocuments.stream().map(myDoc ->
(String) myDoc.getMetadata().get(MetaData.ID)).map(myUuid ->
UUID.fromString(myUuid)).toList())
.stream().collect(Collectors.toMap(myDoc -> myDoc.getId(),
myDoc -> myDoc));
return imageMap.entrySet().stream().map(myEntry ->
createImageContainer(aiDocuments, myEntry))
.sorted((containerA, containerB) ->
containerA.distance().compareTo(containerB.distance()))
.map(myContainer -> new ImageDto(myContainer.document().getContent(),
Base64.getEncoder().encodeToString(
myContainer.image().getImageContent()),
myContainer.image().getImageType())).limit(this.resultSize)
.toList();
}
private ImageContainer createImageContainer(List<Document> aiDocuments,
Entry<UUID, Image> myEntry) {
return new ImageContainer(
createIdFilteredStream(aiDocuments, myEntry)
.findFirst().orElseThrow(),
myEntry.getValue(),
createIdFilteredStream(aiDocuments, myEntry).map(myDoc ->
(Float) myDoc.getMetadata().get(MetaData.DISTANCE))
.findFirst().orElseThrow());
}
private Stream<Document> createIdFilteredStream(List<Document> aiDocuments,
Entry<UUID, Image> myEntry) {
return aiDocuments.stream().filter(myDoc -> myEntry.getKey().toString()
.equals((String) myDoc.getMetadata().get(MetaData.ID)));
}
private ResultData createAIResult(ImageQueryDto imageDto) {
if (ImageType.JPEG.equals(imageDto.getImageType()) ||
ImageType.PNG.equals(imageDto.getImageType())) {
imageDto = this.resizeImage(imageDto);
}
var prompt = new Prompt(new UserMessage(imageDto.getQuery(),
List.of(new Media(MimeType.valueOf(imageDto.getImageType()
.getMediaType()), imageDto.getImageContent()))));
var response = this.chatClient.call(prompt);
var resultData = new
ResultData(response.getResult().getOutput().getContent(), imageDto);
return resultData;
}
private ImageQueryDto resizeImage(ImageQueryDto imageDto) {
...
}
}
在‘importImage(…)’方法中,將調(diào)用‘createAIResult(…)’方法。它檢查圖像類型,并調(diào)用‘resizeImage(…)’方法將圖像縮放到LLava模型支持的大小。然后使用提示文本和包含圖像、媒體類型和圖像字節(jié)數(shù)組的媒體創(chuàng)建Spring AI Prompt。然后,‘chatClient’調(diào)用提示,并在‘ResultData’記錄中返回響應(yīng),其中包含描述和調(diào)整大小的圖像。然后將調(diào)整大小的圖像添加到圖像實體中,并保留該實體。然后,在元數(shù)據(jù)中創(chuàng)建了包含嵌入、描述和圖像實體ID的AI文檔。最后使用描述、調(diào)整大小的圖像和圖像類型創(chuàng)建ImageDto并返回。
在‘queryImage(…)’方法中,檢索并過濾元數(shù)據(jù)中圖像類型的AI文檔的最低距離的Spring AI文檔。按照最小的距離對文檔進(jìn)行排序。然后加載帶有Spring AI文檔元數(shù)據(jù)ID的圖像實體。這樣就可以創(chuàng)建具有匹配文檔和圖像實體的ImageDtos。圖像以Base64編碼字符串的形式提供。這使得MediaType可以輕松地在IMG標(biāo)簽中顯示圖像。
要顯示Base64 Png圖像,可以使用:‘<img src=”data:image/png;base64,iVBORw0KG…” />’
結(jié)果
用戶界面(UI)的結(jié)果見下圖:
該應(yīng)用程序使用嵌入在向量數(shù)據(jù)庫中找到大型飛機。選擇第二張圖像是因為天空相似,而圖像搜索只花了不到一秒的時間。
結(jié)論
Spring AI和Ollama的支持為用戶提供了使用免費LLava模型的機會,這使得該圖像數(shù)據(jù)庫的實現(xiàn)變得容易。LLava模型可以生成圖像的良好描述,這些描述可以轉(zhuǎn)換為嵌入以進(jìn)行快速搜索。Spring AI缺少對生成API端點的支持,因為參數(shù)‘spring.ai.ollama.chat.options.keep_alive=1s’需要Keep_alive =1s '來避免上場景窗口中有舊數(shù)據(jù)。LLava模型需要GPU加速才能有效使用。LLava僅用于導(dǎo)入,這意味著描述的創(chuàng)建可以異步完成。在中等性能的筆記本電腦上,LLava模型在一個CPU上運行,每張圖像的處理時間為5~10分鐘。與以前的實現(xiàn)相比,這樣的圖像搜索解決方案是一個巨大的進(jìn)步。隨著采用更多GPU或CPU對人工智能的支持,這樣的圖像搜索解決方案將變得更加流行。
原文標(biāo)題:Questioning an Image Database With Local AI/LLM on Ollama and Spring AI,作者:Sven Loesekann