1、图片搜索的产品形式
图片搜索的产品形式非常简单,上传一张图,搜索出与原图相似度较高的图,比如百度的图搜:
https://image.baidu.com/
接下来,我们就搭建一个简易版的图搜服务(受限于没有服务器,我就在mac上跑了)
2、golang webserver框架选型
golang的webserver框架,小而美的首推gin,也是当下golang圈子里最流行的框架,官方文档如下:
https://gin-gonic.com/zh-cn/docs/
github地址:
https://github.com/gin-gonic/gin
安装gin:
go get -u github.com/gin-gonic/gin
gin是一个使用golang开发的web框架,性能极佳,开发成本低,中文文档丰富,github上拥有近80k的star,是golang开发者的首选。
gin框架开发hello world版本:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
框架选型完成后,接下来就是要设计Restful api了。
3、图搜restful api设计
图像检索服务吗,肯定是以图搜图,所以api的设计上,api输入为图片,单张多张技术上都没有问题,我们本次的教程就设置为一张,之后返回和这张图片类似的top k张图。
api url设计为post请求,url定义如下:
mianhuatang/image_search
路由设置代码参考如下:
func Register(engine *gin.Engine) {
engine.Use(middlewares.Print)
engine.POST("mianhuatang/image_search", controllers.ImageSearch)
}
接口输入,只接收一张图:
参数名称 | 类型 | 说明 |
file | 文件类型 | 用于上传图片 |
接口输出,返回所有与输入图片相似的图片的链接,这个链接最好是对象存储的地址,这样方便前端直接展示搜索结果,百度的BOS,阿里的OSS都是不错的选择,接口返回的数据结构如下:
{
"err_num": 0,
"err_msg": "操作成功",
"data": [
{
"image_url": "cat.353.jpg",
"name": "cat_823"
},
{
"image_url": "cat.127.jpg",
"name": "cat_579"
}
]
}
ok,接口设计完成,接下来看如何抽取图片特征。
4、图片特征在线提取
在我的另一篇文章《吭哧一天才搞定:elasticsearch向量检索需要的数据集以及768维向量生成》,我们找了一个openai的开源模型,使用python实现了特征的提取,在这里我们仍旧使用这个方案,只不过土鳖一点,在go的代码里以命令行的方式去调用这个python脚本(后面尝试用tritonserver工程化这个模型,我去实现一个自定义的bankend,大家可以蹲个后续),首先对这个脚本稍加修改:
1、脚本支持命令行参数,输入一个路径。
2、服务把api上传的图片保存到某个地方,之后调用脚本,输入路径,提取图片特征。
要增加的代码主要就是接收命令行参数,其他变化不大:
if __name__ == "__main__":
args = sys.argv[1:] # 获取所有传递给脚本的参数
image_path = args[0]
main(image_path)
完整代码我会放到文章最后。
5、golang部分实现逻辑
当server端将图片保存到本地后,调用python脚本抽取特征,因为是在golang里调用python脚本运行,go只能去捕获脚本输出以得到图片特征向量,在python脚本里通过print将向量进行打印,golang捕获字符串,之后再转换为float64类型的切片,python脚本的输出类似这样:
[[-0.011099960654973984, 0.05776820704340935, 0.03730260953307152, 0.03493053838610649]]
go核心部分代码如下:
func ImageSearch(ctx *gin.Context, fileName string, response *result.JsonResponseString) {
floatVec := make([]float64, 0)
curPath, _ := os.Getwd()
// 运行 Python 脚本并捕获输出
cmd := exec.Command("python3", curPath+"/features/feature.py", curPath+"/images/")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
fmt.Println("Error running Python script:", err)
return
}
// 打印 Python 脚本的输出
vecs := out.String()
start := strings.LastIndex(vecs, "[")
end := strings.LastIndex(vecs, "]")
content := vecs[start+1 : end-1]
sliVecs := strings.Split(content, ", ")
// 将字符串切片转换为浮点数切片,拿到特征向量
for _, v := range sliVecs {
valu, _ := strconv.ParseFloat(v, 64)
floatVec = append(floatVec, valu)
}
}
6、执行向量检索
上面章节已经获取到检索图片的特征向量,那么执行检索就简单了,无非就是构造向量检索语句,去es检索即可,核心代码:
func ImageSearch(client *elasticsearch.Client, query string) []map[string]any {
results := make([]map[string]any, 0)
docs := Documents{}
search := esapi.SearchRequest{
Index: []string{"vector_search_202412"},
Body: strings.NewReader(query),
}
resp, err := search.Do(context.Background(), client)
if err != nil {
fmt.Println("search err:", err.Error())
return results
}
err = json.NewDecoder(resp.Body).Decode(&docs)
if err != nil {
fmt.Println("decode err:", err.Error())
return results
}
for _, v := range docs.Hits.Hits {
result := make(map[string]any)
result["name"] = v.Source["name"]
result["image_url"] = v.Source["path"]
results = append(results, result)
}
return results
}
构建dsl代码:
mapQuery := map[string]any{
"size": 10,
"query": map[string]any{
"knn": map[string]any{
"field": "IFV",
"k": 10,
"query_vector": floatVec,
},
},
}
dsl, _ := json.MarshalToString(mapQuery)
results := data.ImageSearch(resource.ElasticClient, dsl)
接口的返回示例:
到这里,整个图片检索的服务就开发完成了,实际项目中,完善下代码健壮性可以直接拿去用。
代码已上传到github,地址:
https://github.com/liupengh3c/career
最后欢迎各位小伙伴关注公众号【码农夜读】,可以免费获得一份《c++ primer plus》视频教程一份。
另外可以帮助下载 csdn 上 vip 资源(不要钱,😄),可以在公众号加微信,发链接,我来下载,之后发给你。