Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

最近碰到一个问题,因为一些干扰因素导致排查的时候走了一段歧路,
报错信息是

1
Exception in thread "main" com.alibaba.fastjson.JSONException: scan null error

用一个简单的demo来复现下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public enum DemoEnum {

DEMO1("demo1", "desc");
private String code;

private String desc;

DemoEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static DemoEnum getByCode(String code) {
for (DemoEnum demoEnum : values()) {
if (demoEnum.code.equals(code)) {
return demoEnum;
}
}
return null;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}
}

先定义一个枚举类,然后有个demo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FastJsonDemo implements Serializable {

private static final long serialVersionUID = 1131767138182111892L;
private String name;

private Map<DemoEnum, String> map = new HashMap<>();

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Map<DemoEnum, String> getMap() {
return map;
}

public void setMap(Map<DemoEnum, String> map) {
this.map = map;
}
}

其中map的key是上面定义的枚举类,然后在main方法中做一下序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
public class demo {

public static void main(String[] args) {
FastJsonDemo fastJsonDemo = new FastJsonDemo();
fastJsonDemo.setName("nick");
Map<DemoEnum, String> map = new HashMap<>();
map.put(DemoEnum.getByCode("code"), "null key value");
fastJsonDemo.setMap(map);
FastJsonDemo decodeFastJsonDemo = JSONObject.parseObject(JSONObject.toJSONString(fastJsonDemo), FastJsonDemo.class);
}
}

这样就会出现这个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Exception in thread "main" com.alibaba.fastjson.JSONException: scan null error
at com.alibaba.fastjson.parser.JSONLexerBase.scanNullOrNew(JSONLexerBase.java:4531)
at com.alibaba.fastjson.parser.JSONLexerBase.nextToken(JSONLexerBase.java:154)
at com.alibaba.fastjson.parser.JSONLexerBase.nextToken(JSONLexerBase.java:358)
at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:227)
at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:61)
at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:41)
at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_FastJsonDemo.deserialze(Unknown Source)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:269)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:671)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:365)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:269)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:488)
at fastjson.demo.main(demo.java:23)

这个异常有两个原因,第一当然是在map中出现了null作为key的数据,第二就是这个key是复杂对象
在反序列化以后就会出现这个异常,因为最初出现这个异常是因为我改了另一个字符串字段,并且会反序列化成json就让我判断出现了误差
这边做一个记录。

上次我们尝试用 towheemilvus 实现了图片的向量化,那么顺势我们就能在这个基础上实现以图搜图
首先我们找一些图片,

然后我们先把他们都向量化,存储在milvus里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from towhee import AutoPipes, AutoConfig, ops
import towhee
import os
from pymilvus import MilvusClient
import json

# 1. 设置一个Milvus客户端
client = MilvusClient(
uri="http://localhost:19530"
)
insert_conf = AutoConfig.load_config('insert_milvus')
insert_conf.collection_name = 'text_image_search'


insert_pipe = AutoPipes.pipeline('insert_milvus', insert_conf)

# 创建图像嵌入管道
image_embedding_pipe = AutoPipes.pipeline('image-embedding')
files = os.listdir("./images")
for file in files:
file_path = os.path.join("./images", file)
if os.path.isfile(file_path):
embedding = image_embedding_pipe(file_path).get()[0]
insert_pipe([file_path, embedding])

然后我们找一张神仙姐姐的其他图片,先把它 embedding 后在 milvus 里进行向量检索

1
2
3
4
5
6
7
8
9
10
11
12
13
image_embedding_pipe = AutoPipes.pipeline('image-embedding')

# 生成嵌入
embedding = image_embedding_pipe('./to_search.jpg').get()[0]
print(embedding)
res = client.search(
collection_name="text_image_search",
data=[embedding],
limit=5,
search_params={"metric_type": "IP", "params": {}}
)
result = json.dumps(res, indent=4)
print(result)

我们检索出来5个结果,

可以通过distance找到距离最近的这个是id=451291280409722600
可以发现也是神仙姐姐的,只是作为参考
to_search 目标图片是

搜索的最短距离就是id对应的图片

这张图片就是神仙姐姐

之前觉得谷歌的以图搜图很厉害,现在似乎这个路径还毕竟清晰了,首先要有图片库,把它们向量化以后存储起来,然后对于目标图片也做向量化,再做检索
那么我们先来做重要的这一步,图片的向量化,因为向量化以后就跟图片没关系了,直接用前面讲到的向量的近似搜索就可以做到以图搜图了
这边我们用到了towhee工具,towhee是个机器学习的pipeline工具,可以做数据源(文件,图片,媒体,文本)–> 模型 –> 向量。
首先我们安装towhee

1
pip install towhee towhee.models

这里有第一个坑,因为有torch依赖,但是目前torch支持的python版本是最高3.9,再往上可能就有问题了
所以我们要先创建一个3.9的环境

1
conda create -n py9 python=3.9

然后我们会遇到第二个坑
就是milvus的客户端

1
pip install pymilvus==2.3.0

再早的客户端会有依赖不支持,更新的客户端会连接不上
然后再在milvus创建一个Collection

然后这个是不对的,因为我使用attu界面创建的,默认第二个字段一定得是向量字段,所以又用了上次的Java代码来创建

1
2
3
4
5
6
7
8
9
10
11
12
13
CreateCollectionReq.CollectionSchema collectionSchema = clientV2.createSchema();
// add two fileds, id and vector
Integer dim = 2048;
collectionSchema.addField(AddFieldReq.builder().fieldName("url").dataType(DataType.VarChar).build());
collectionSchema.addField(AddFieldReq.builder().fieldName("embedding").dataType(DataType.FloatVector).dimension(dim).build());
collectionSchema.addField(AddFieldReq.builder().fieldName("id").dataType(DataType.Int64).isPrimaryKey(Boolean.TRUE).autoID(Boolean.TRUE).description("id").build());

CreateCollectionReq req = CreateCollectionReq
.builder()
.collectionSchema(collectionSchema)
.collectionName("text_image_search")
.dimension(dim).build();
clientV2.createCollection(req);

并且注意字段顺序不能错,这个顺序在towhee的demo中也没地方直接修改
看一下towhee的demo
地址在这
https://towhee.io/tasks/detail/pipeline/text-image-search

1
2
3
4
5
6
7
8
9
10
11
12
13
from towhee import AutoPipes, AutoConfig

# set MilvusInsertConfig for the built-in insert_milvus pipeline
insert_conf = AutoConfig.load_config('insert_milvus')
insert_conf.collection_name = 'text_image_search'

insert_pipe = AutoPipes.pipeline('insert_milvus', insert_conf)

# generate embedding
embedding = image_embedding('./test1.png').get()[0]

# insert text and embedding into Milvus
insert_pipe(['./test1.png', embedding])

第三个坑,这并不能跑起来

1
2
3
 embedding = insert_pipe.image_embedding('./test.jpg').get()[0]
^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'RuntimePipeline' object has no attribute 'image_embedding'

因为找不到这个 image_embedding
估计是这个也是包路径变更了,但是并没有什么文档可以找到,之前LlamaIndex还有点更新指南,后来是问了Claude才知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from towhee import AutoPipes, AutoConfig, ops


# set MilvusInsertConfig for the built-in insert_milvus pipeline
insert_conf = AutoConfig.load_config('insert_milvus')
insert_conf.collection_name = 'text_image_search'


insert_pipe = AutoPipes.pipeline('insert_milvus', insert_conf)

# 创建图像嵌入管道
image_embedding_pipe = AutoPipes.pipeline('image-embedding')

# 生成嵌入
embedding = image_embedding_pipe('./test.jpg').get()[0]


# insert text and embedding into Milvus
insert_pipe(['./test.jpg', embedding])

需要从 AutoPipes 中把这个 image-embedding 找出来
然后就是上面说到的字段映射问题,默认是先url,再embedding字段,并且主键字段必须是autoId,不然也会缺少默认值,还是感叹下,工程化的东西还是要工程化的质量保证,否则变更都无从知晓,现在人工智能大热,大家都在追风,只是基础的软件还是要稳扎稳打,这样我们工程人员才能把它们更好的用起来,PS:娃真可爱,但真的好累

之前做了个简单的铺垫,作为大模型应用技术领域非常重要的一环,向量数据库我们在前面有做一些引导性的介绍,其中的索引技术,
而在众多向量数据库比较有代表性的 Milvus,这边我们来尝试安装 Milvus 初体验一下
因为只是初体验,不作为生产环境使用,所以就用最简单的方式,Docker单机部署的方式
首先需要需要有Docker环境
确认下 docker 命令可执行
然后拉取启动脚本

1
2
wget https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh
bash standalone_embed.sh start

启动过程中需要注意一些问题,这也是有点想吐槽的,这个官方的启动脚本竟然都不是稳定成功的,最开始就是失败的,后面重新删除拉下来的镜像再启动就好了
并且官方提供的demo python版本的示例也是有问题的
因为主要是java的缘故,就用了java的sdk来尝试
只是我们可以先运行一个ui工具

1
docker run -p 8000:3000 -e MILVUS_URL={milvus server IP}:19530 zilliz/attu:v2.4


这里就能看到运行情况
然后我们运行个简单的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package MilvusDemo;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.DataType;
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
import io.milvus.v2.service.index.request.CreateIndexReq;
import io.milvus.v2.service.vector.request.InsertReq;

import java.util.ArrayList;
import java.util.List;

/**
* @author shixuesen
* @date 2024/7/21
*/
public class Demo {

public static void main(String[] args) {
ConnectConfig connectConfig = ConnectConfig.builder()
.uri("http://127.0.0.1:19530")
.build();
// 连接配置
MilvusClientV2 clientV2 = new MilvusClientV2(connectConfig);
// define a Collection Schema
CreateCollectionReq.CollectionSchema collectionSchema = clientV2.createSchema();
// add two fileds, id and vector
Integer dim = 5;
// 这里的 CollectionSchema 就类似于mysql的数据库表结构
collectionSchema.addField(AddFieldReq.builder().fieldName("my_id").dataType(DataType.Int64).isPrimaryKey(Boolean.TRUE).autoID(Boolean.FALSE).description("id").build());
collectionSchema.addField(AddFieldReq.builder().fieldName("my_vector").dataType(DataType.FloatVector).dimension(dim).build());

CreateCollectionReq req = CreateCollectionReq
.builder()
.collectionSchema(collectionSchema)
.collectionName("quick_setup")
.dimension(dim).build();
// 通过schema 生成 Collection,就类似于mysql中的表
clientV2.createCollection(req);

// 然后可以对字段创建索引
IndexParam indexParam = IndexParam.builder()
.fieldName("my_id")
.build();
IndexParam vertorIndexParam = IndexParam.builder()
.fieldName("my_vector")
.indexType(IndexParam.IndexType.AUTOINDEX)
.metricType(IndexParam.MetricType.IP)
.build();
List<IndexParam> indexParamList = new ArrayList<>();
indexParamList.add(indexParam);
indexParamList.add(vertorIndexParam);


// 接着创建索引
clientV2.createIndex(CreateIndexReq.builder()
.collectionName("quick_setup")
.indexParams(indexParamList)
.build());
JsonObject jsonObject = JsonParser.parseString("{\"my_id\": 0, \"my_vector\": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592], \"color\": \"pink_8682\"}").getAsJsonObject();
List<JsonObject> jsonData = new ArrayList<>();
jsonData.add(jsonObject);
// 然后插入数据
clientV2.insert(InsertReq.builder().collectionName("quick_setup")
.data(jsonData).build());

// 插入之后我们想进行查询,就用向量进行搜索,不过要先加载Collection
clientV2.loadCollection(LoadCollectionReq.builder().collectionName("quick_setup").build());
SearchResp searchResp = clientV2.search(SearchReq.builder()
.collectionName("quick_setup")
.data(Collections.singletonList(new FloatVec(new float[]{0.3580376395471989F, -0.6023495712049978F, 0.18414012509913835F, -0.26286205330961354F, 0.9029438446296592F})))
.topK(10)
.outputFields(Collections.singletonList("*"))
.build()
);
for (List<SearchResp.SearchResult> searchResult : searchResp.getSearchResults()) {
System.out.println(searchResult);
}


}
}

我们用同样的向量进行搜索

1
[SearchResp.SearchResult(entity={my_id=0, $meta={"color":"pink_8682"}, my_vector=[0.35803765, -0.6023496, 0.18414013, -0.26286206, 0.90294385]}, score=1.4093276, id=0)]

得出的score就是很高的,只是做了下尝试,官方的示例代码是少得可怜,也不全,很难想象是目前比较热门的向量数据库

最近在迁移一个自己用的mysql实例,发现用 portainer 安装 mysql 一直失败,还以为是配置了自定义端口映射被系统防火墙限制,但后面不映射端口也是不行,一开始查看 journal日志也没什么发现,因为之前在腾讯云的小机器也是正常的部署,所以没觉得是可能会oom,一时间就有点没头绪,结果也是好奇就看了下dmesg,发现里面都是oom,给了1g的内存的ubuntu虚拟机,安装了一两个docker就不行了,不过这里比较重要的就是记录下dmesg 跟 journal日志的这点区别
dmesg 打印了内核的日志缓冲,所以这个跟journal差别的第一点就是一个是个会被刷掉的缓冲区,另一个是记在文件,但更重要的是dmesg是打印的内核日志,而journal是会通过systemd收集日志,journal可以收集内核日志,但是可以进行配置,并且可能会晚于dmesg

而对于这个oom,如果是系统的oom killer,那的确是有可能dmesg会能查看到,而journal看不到
简单记录下,PS:奶娃是有点累,一天都没啥时间了

0%