基于Es和智普AI实现的语义检索
1、什么是语义检索
语义检索是一种利用自然语言处理(NLP)和人工智能(AI)技术来理解搜索查询的语义,以提供更准确和相关搜索结果的搜索技术,语义检索是一项突破性的技术,旨在通过深入理解单词和短语的含义及上下文来提供更为准确的搜索结果。与传统的基于关键词的搜索不同,语义检索侧重于查询的意图和语境,而不仅仅是关键词的直接匹配。这种搜索方式可以显著提高搜索结果的准确性和相关性,尤其适用于复杂查询和模糊不清的搜索需求
上面是比较"官方"的解释,举个例子来简单说明一下吧:
比如现在有一条数据,叫诸葛亮,输入关键字孔明,传统的查询(关系型数据)和搜索引擎(Elasticsearch)是查不出来诸葛亮这条数据的,因为这二者都是通过关键字来匹配的,但是语义检索能够查出来,这就是语义检索最大的亮点
2、语义检索能做什么
除了上面提到的文字检索,还可以实现基于文档内容(本质还是文字检索)、图片、和视频内容的检索
3、语义检索的实现思路
Elasticsearch在7.X版本开始支持knn查询,也就是近似查询(也叫向量查询),关于knn的深入理解就不在这里过多的赘述了,其底层原理我也不是很懂,整体的思路如下图:
其中图片和视频的转换为向量的时候有两种方式,一种是直接将图片和视频转换为向量,这种相对较复杂,第二种是通过把图片和视频交给大模型,让其理解其中的内容,并将内容提取出来转换为文字,然后将文字再转换为向量存入es
4、操作实践
准备工作:
Elasticsearch 8.12.0
智普AI向量转换:智谱AI开放平台(embedding-3)
1、建立Es索引mapping,其中几个关键的属性字段:
dims:向量的纬度数,本质就是一个float类型的数组,我自己测试的结果是1024效果比较好,一般这个值有738,1024,2048,具体要看模型支持多少唯独
type:int8_hnsw,这个是es8.8才提供的特性
{
"my_embedding": {
"mappings": {
"dynamic": "strict",
"properties": {
"department": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "my_analyzer"
},
"embedding": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "l2_norm",
"index_options": {
"type": "int8_hnsw",
"m": 16,
"ef_construction": 100
}
}
}
}
}
}
2、通过智普AI生成向量数据写入Elasticsearch:Authorization填写自己申请的智普的授权码
@Test
public void vectorWriteV1() throws Exception {
// List<String> text = Stream.of("刘玄德", "桃园三结义", "刘备", "毛岸英", "毛泽东思想", "新中国", "江青", "杨开慧", "毛润之", "三国", "武当山", "二万五千里长征", "太极拳").collect(Collectors.toList());
List<String> text = Stream.of(
// "武当山不仅是中国道教文化的圣地,也是自然与人文景观的完美融合之地。游客在这里可以体验丰富的历史文化和壮丽的自然风光",
// "庐山集自然美景与深厚文化底蕴于一体,无论是自然景观的雄奇险秀,还是人文景观的丰富多彩,基本都使其成为不可多得的旅游胜地。对于想要探访庐山的游客来说,春夏秋三季是最佳游览时节。"
// "西安事变是**由张学良和杨虎城于1936年12月12日为劝谏蒋介石改变“攘外必先安内”的国策、停止内战一致抗日而发动的兵谏,又称双十二事变**。这次事变对我国近现代史产生了深远的影响,不仅因为其直接参与者的身份特殊,更因为它标志着国内战争向抗日战争转折的重大历史节点"
// "大势智慧科技有限公司凭借其在实景三维数字化重建及三维数据服务的深厚技术积累和丰富的实践经验,已经成为该领域的领导者。面对未来的机遇与挑战,大势智慧有望继续发挥其在技术创新和行业应用方面的优势,为不同行业提供更多高质量的三维数字化解决方案,推动相关领域的技术进步和产业升级"
"关羽", "张飞"
).collect(Collectors.toList());
String url = "https://open.bigmodel.cn/api/paas/v4/embeddings";
Map<String, String> header = new HashMap<>();
header.put("Content-Type", "application/json");
header.put("Authorization", "");
for (String s : text) {
List<Float> embeddings = new LinkedList<>();
EmbeddingParams embeddingParams = CommonBuilder.of(EmbeddingParams.class)
.with(EmbeddingParams::setInput, ListBuilder.<String>ofList().add(s).build())
.build();
String result = HttpRequestUtil.doPost(url, header, JSON.toJSONString(embeddingParams));
JSONObject jsonObject = JSON.parseObject(result);
JSONArray jsonArray = jsonObject.getJSONArray("data");
JSONObject embeddingObj = (JSONObject) jsonArray.get(0);
JSONArray embedding = embeddingObj.getJSONArray("embedding");
for (Object o : embedding) {
embeddings.add(((BigDecimal) o).floatValue());
}
MyVector myVector = CommonBuilder.of(MyVector.class)
.with(MyVector::setDepartment, s)
.with(MyVector::setEmbedding, embeddings)
.build();
esClientUtil.writeData(myVector, IdUtil.getSnowflakeNextId() + "", "my_embedding");
}
}
3、测试验证knn检索:
@Test
public void vectorSearchV1() throws Exception {
// List<String> text = Stream.of("刘备", "关羽", "毛泽东思想", "毛泽东", "林彪", "周恩来", "张三丰", "十堰").collect(Collectors.toList());
// List<String> text = Stream.of("西安", "十堰", "江西", "张学良", "蒋介石", "杨虎城", "黄先锋").collect(Collectors.toList());
List<String> text = Stream.of("桃园三结义", "十堰", "江西", "张学良", "蒋介石", "大势智慧", "黄先锋").collect(Collectors.toList());
String url = "https://open.bigmodel.cn/api/paas/v4/embeddings";
Map<String, String> header = new HashMap<>();
header.put("Content-Type", "application/json");
header.put("Authorization", "");
for (String s : text) {
List<Float> embeddings = new LinkedList<>();
EmbeddingParams embeddingParams = CommonBuilder.of(EmbeddingParams.class)
.with(EmbeddingParams::setInput, ListBuilder.<String>ofList().add(s).build())
.build();
String result = HttpRequestUtil.doPost(url, header, JSON.toJSONString(embeddingParams));
JSONObject jsonObject = JSON.parseObject(result);
JSONArray jsonArray = jsonObject.getJSONArray("data");
JSONObject embeddingObj = (JSONObject) jsonArray.get(0);
JSONArray embedding = embeddingObj.getJSONArray("embedding");
for (Object o : embedding) {
embeddings.add(((BigDecimal) o).floatValue());
}
// 设置查询向量
KnnQuery.Builder builder = new KnnQuery.Builder();
builder.field("embedding")
.numCandidates(100)
.k(10)
// .similarity(0.3f)
.queryVector(embeddings);
// 创建搜索请求
SearchRequest searchRequest = new SearchRequest.Builder()
.index("my_embedding")
.knn(builder.build())
.size(10)
.build();
// 执行搜索请求
SearchResponse<Product> searchResponse = es8ClientUtil.searchData(searchRequest, "my_embedding", Product.class);
StringJoiner stringJoiner = new StringJoiner("\t");
StringJoiner score = new StringJoiner("\t");
for (Hit<Product> hit : searchResponse.hits().hits()) {
stringJoiner.add(hit.source().getDepartment());
score.add(hit.score() + "");
}
System.err.println(s + "===检索结果===:");
System.err.println(stringJoiner);
System.err.println(score);
System.err.println();
}
}
其中用到的HttpRequestUtil是我自己基于apachehttpclient封装的工具类:基于ApacheHttpclient封装的请求工具类(记笔记)_java apache请求工具类-CSDN博客
PS:经过几轮测试下来,得出以下结论:
1、knn检索确实可以实现语义检索,弥补传统的关键字检索的不足,二者结合可以实现更加智能的检索
2、knn检索的"准确性"问题,比如我在测试的时候输入林彪,也能查询到诸葛亮这条数据,在我们的认知里面,这两个人是八竿子打不着的, 但是在经过模型转换之后,他们之间可能确实存在一些相似性,比如都是历史名人,这个"偏差"很依赖于模型的理解和转换能力
3、我测试过智普、阿里云、百度千帆和百川智能四个模型,总体上智普的效果比较好