本文将介绍如何使用OpenSearch 向量检索版和智能问答版,搭建灵活自定义的企业专属对话搜索系统。
OpenSearch LLM智能问答版提供了多种LLM、向量化模型、切片模型等丰富的内置模型,帮助企业快速搭建专属对话式搜索服务。但在数据结构自定义、搜索索引构建与召回环节,智能问答版开放的自定义能力有限,暂时无法支持在原有内置字段基础上增加自定义字段以及根据自定义字段构建过滤、排序规则。
环境准备
第一次开通阿里云账号并登录控制台时,会提示先创建access key才能继续使用。
- 创建及使用应用依赖access key参数,主账号下access key参数不能为空。
- 在为主账号创建access key参数后,还可以再创建RAM子账号access key通过RAM子账号进行访问。
配置流程概述
具体操作流程如下图所示:
向量检索版实例负责:
- 自定义数据结构,以及基于这些自定义数据结构的过滤,排序
- 存储用户原始文档数据,向量数据
- 向量数据的召回
智能问答版实例负责:
- 对用户原始文档进行切片和向量化
- 对用户原始query进行向量化
- 大模型提供问答能力
重要
该方案需购买向量检索版实例和智能问答版实例
创建和配置向量检索版实例
购买OpenSearch向量检索版实例
购买实例可参考购买OpenSearch向量检索版实例。
配置OpenSearch向量检索版实例
新购买的实例,在其详情页中,实例状态为“待配置”,并且会自动部署一个与购买的查询节点和数据节点的个数及规格一致的空集群,之后需要为该集群配置数据源--->配置索引--->索引重建,之后才可正常搜索。
1、表基础信息
表管理点击“表添加",输入表名称,设置数据分片数和数据更新资源数,选择需要的场景模板,点击下一步:
配置说明:
- 表名称:可自定义。
- 数据分片数:分片数设置时,各索引表分片数需保持一致;或至少一个索引表分片数为1,其余索引表分片数一致。
- 数据更新资源数:数据更新所用资源数,每个索引默认免费提供2个4核8G的更新资源,超出免费额度的资源将产生费用。
- 场景模板:向量检索版内置了3种模板可供用户选择:通用、向量-图片搜索、向量-文本语义模板。
2、 数据同步
配置数据源(目前支持的数据源有MaxCompute数据源和API推送数据源),这里以API推送数据源为例,选择API推送数据源,点击下一步。
- MaxCompute + API 数据源文档参考。
- API 数据源文档参考。
3、 字段配置
OpenSearch会根据您选择的场景模板,预置相关字段,并会将全量数据来源中的字段(如有),自动导入字段列表中:
设置字段,必须包含至少两个字段,主键字段和向量字段(向量字段需要设置为多值float类型):
说明
表结构中必须要包含以下字段:
- 文档主键,如上图中的doc_id;
- 文档切分后内容字段,如上图中的split_content;
- 文档切分后内容向量化值,如上图中的split_content_embedding。该字段需要设置为向量字段,并以","分割。
字段配置说明:
- 必选字段:主键字段和向量字段,主键字段为int或string类型并且需要勾选主键按钮,向量字段为float类型并且需要勾选向量字段按钮;
- 向量字段默认为多值的float类型,多值分隔符默认使用ha3分割符^] 进行切分(其对应utf编码为\x1D),也可以输入自定义多值分隔符
- 向量字段的高级配置中,如无需向量化,则选择“无需向量embedding处理”,如有其他需求,可选择clip图片转向量,或ops-text文本转向量。
- 使用向量检索,在定义字段时有位置要求,需要按照主键字段、标签字段(非必要)、向量字段的顺序创建。(如上图所示)
4、索引结构
向量索引
Opensearch已为非向量字段,自动构建单字段索引,索引名与字段名相同,只需要在控制台配置向量索引:
配置说明:
- 向量维度:需要改成1536维
- 其他参数:保持默认即可
- 主键字段、向量字段必须填写,标签字段非必填,可以为空。
- 仅支持选择固定的三个字段,不支持新增。
5、确认创建
索引配置完成后,点击确认创建。
创建和配置智能问答版实例
购买和配置OpenSearch智能问答版实例
1、进入OpenSearch控制台,区域选择“华东2(上海)”在左上角切换到LLM智能问答版:
2、进入控制台后,在实例管理界面,点击创建实例:3、在创建实例页面,选择LLM智能问答版,选择区域填写实例名称,并选择数据的存储容量,点击立即购买:
4、在确认订单页面,查看服务协议,确认无误后,点击立即开通:
5、购买成功后,点击管理控制台,即可在实例管理界面查看已购买的LLM智能问答版实例:
说明
OpenSearch-LLM智能问答版购买成功后无需在额外进行配置。
数据入库
对文档内容进行切片和向量化
当前向量化模型最大支持300 token,一般情况下原始文档内容通常会比较大,因此需要对原始文档内容进行切片。且需要对切片之后的文档内容进行向量化。
将切片和向量化之后的文档推送到向量检索版实例
1、将上述文本切片向量化结果通过拼接组合的方式生成新主键,相关字段映射向量检索版内对应字段,详情如下:
- chunk_id为切分后的文档id。需要和原始文档id拼接起来,组成新的文档主键:doc_id
- chunk为切分后文档内容,对应split_content;
- embedding为切分后文档内容向量化值,对应split_content_embedding;
2、将上述结果推送到向量检索版实例。
3、调用向量检索版实例相关endpoint和api,请参考代码示例或数据推送 Demo
查询问答
1、query向量化
简介:将用户原始查询内容进行向量化。
2、召回向量化结果数据
使用query的向量化的结果,去向量检索版进行召回。通常来说召回top5的结果即可。
3、大模型问答
基于向量检索版召回的结果,可调用智能问答版的大模型对话接口,进行问答。
详情可参考:代码示例 或 knowledge-llm-大模型对话接口
代码示例
使用Java SDK完成数据推送和智能问答的demo如下:
maven依赖
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-sdk-ha3engine</artifactId> <version>1.3.4</version> </dependency> <dependency> <groupId>com.aliyun.opensearch</groupId> <artifactId>aliyun-sdk-opensearch</artifactId> <version>4.0.0</version> </dependency>
Java Demo
import com.aliyun.ha3engine.Client; import com.aliyun.ha3engine.models.*; import com.aliyun.opensearch.OpenSearchClient; import com.aliyun.opensearch.sdk.dependencies.org.json.JSONObject; import com.aliyun.opensearch.sdk.generated.OpenSearch; import com.aliyun.opensearch.sdk.generated.commons.OpenSearchClientException; import com.aliyun.opensearch.sdk.generated.commons.OpenSearchException; import com.aliyun.opensearch.sdk.generated.commons.OpenSearchResult; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** *整合向量检索版和智能问答版demo */ public class LLMDemo { /** * 智能问答版实例应用名称 */ private static String llmAppName = "xxx"; /** * 智能问答版实例访问地址 */ private static String llmHost = "http://opensearch-cn-shanghai.aliyuncs.com"; /** * 智能问答版实例访问key */ private static String llmAccessKey = "xxx"; /** * 智能问答版实例访问秘钥 */ private static String llmAccessSecret = "xxx"; /** * 向量检索版实例API域名 */ private static String embeddingEndpoint = "ha-cn-xxx.public.ha.aliyuncs.com"; /** * 向量检索版实例名称 */ private static String embeddingInstanceId = "ha-cn-xxx"; /** * 向量检索版文档推送的数据源配置名称. */ private static String embeddingTableName = "ha-cn-xxx_llm"; /** * 向量检索版文档索引表名称. */ private static String embeddingIndexName = "llm"; /** * 向量检索版文档推送的文档主键字段. */ private static String embeddingPkField = "doc_id"; /** * 向量检索版用户名 */ private static String embeddingUserName = "xxx"; /** * 向量检索版密码 */ private static String embeddingPassword = "xxx"; public static void main(String[] args) throws Exception { //1.创建OpenSearch LLM智能问答版实例、向量检索版实例的访问对象 //创建访问智能问答版实例的对象 OpenSearch openSearch = new OpenSearch(llmAccessKey, llmAccessSecret, llmHost); OpenSearchClient llmClient = new OpenSearchClient(openSearch); //创建访问向量检索版实例的对象 Config config = new Config(); config.setEndpoint(embeddingEndpoint); config.setInstanceId(embeddingInstanceId); config.setAccessUserName(embeddingUserName); config.setAccessPassWord(embeddingPassword); Client embeddingClient = new Client(config); //2.业务数据推送流程开始 //2.1调用智能问答版对用户原始文档内容进行切分和向量化 Map<String, String> splitParams = new HashMap<String, String>() {{ put("format", "full_json"); put("_POST_BODY", "{\"content\":\"OpenSearch是基于阿里巴巴自主研发的大规模分布式搜索引擎搭建的一站式商用智能搜索平台,目前为包括淘宝、天猫、菜鸟在内的阿里集团核心搜索业务提供中台服务支持。" + "经过多年的行业搜索经验沉淀、双11大促流量冲击,智能开放搜索OpenSearch打磨出一套高性能、高时效、高可用、强稳定搜索全家桶服务,包括LLM智能问答版、行业算法版、高性能检索版、向量检索版、召回引擎版五类商品版本,以满足各行各业的搜索需求。" + "OpenSearch以平台服务化的形式,将专业搜索技术简单化、低门槛化和低成本化,让搜索不再成为客户的业务瓶颈,以低成本实现产品搜索功能并快速迭代\",\"use_embedding\":true}"); }}; String splitPath = String.format("/apps/%s/actions/knowledge-split", llmAppName); OpenSearchResult openSearchResult = llmClient.callAndDecodeResult(splitPath, splitParams, "POST"); System.out.println("split result:" + openSearchResult.getResult()); JsonArray array = JsonParser.parseString(openSearchResult.getResult()).getAsJsonArray(); // 文档推送外层结构, 可添加对文档操作的结构体. 结构内支持一个或多个文档操作内容. ArrayList<Map<String, ?>> documents = new ArrayList<>(); //假设用户原始文档主键为1 String doc_raw_id="001"; for(JsonElement element:array){ JsonObject object = element.getAsJsonObject(); // 添加文档 Map<String, Object> add2Document = new HashMap<>(); Map<String, Object> add2DocumentFields = new HashMap<>(); // 插入文档内容信息, keyValue 成对匹配. // field_pk 字段需与 pkField 字段配置一致. add2DocumentFields.put("doc_id", doc_raw_id+"_"+object.get("chunk_id").getAsString()); add2DocumentFields.put("doc_raw_id", doc_raw_id); add2DocumentFields.put("split_content_embedding", object.get("embedding").getAsString()); add2DocumentFields.put("split_content", object.get("chunk")); add2DocumentFields.put("time", System.currentTimeMillis()); // 将文档内容添如 add2Document 结构. add2Document.put("fields", add2DocumentFields); // 新增对应的文档命令: add add2Document.put("cmd", "add"); documents.add(add2Document); } System.out.println("push docs:"+documents.toString()); //2.2将切分及向量化后的数据推送到向量检索版 PushDocumentsRequestModel requestModel = new PushDocumentsRequestModel(); requestModel.setBody(documents); PushDocumentsResponseModel responseModel = embeddingClient.pushDocuments(embeddingTableName, embeddingPkField, requestModel); String responseBody = responseModel.getBody(); System.out.println("push result:" + responseBody); //数据推送流程结束 //3.在线问答流程开始 //3.1调用智能问答版对用户原始查询语句进行向量化 Map<String, String> embeddingParams = new HashMap<String, String>() { { put("format", "full_json"); put("_POST_BODY", "{\"content\":\"OpenSearch是什么\",\"query\":true}"); }}; String embeddingPath = String.format("/apps/%s/actions/knowledge-embedding", llmAppName); openSearchResult = llmClient.callAndDecodeResult(embeddingPath, embeddingParams, "POST"); System.out.println("query embedding:"+openSearchResult.getResult()); String embedding = openSearchResult.getResult(); //3.2调用向量检索版召回top5匹配结果 SearchRequestModel haQueryRequestModel = new SearchRequestModel(); SearchQuery haRawQuery = new SearchQuery(); // haRawQuery.setQuery("query=doc_id:001_1&&config=start:0,hit:5,format:json&&cluster=general"); haRawQuery.setQuery("query=split_content_embedding:'"+embedding+"'&&config=start:0,hit:5,format:json&&cluster=general"); haQueryRequestModel.setQuery(haRawQuery); SearchResponseModel searchResponseModel = embeddingClient.Search(haQueryRequestModel); System.out.println("result:" + searchResponseModel.getBody()); JsonObject recallResult = JsonParser.parseString(searchResponseModel.getBody()).getAsJsonObject(); long hits = recallResult.get("result").getAsJsonObject().get("numHits").getAsLong(); List<String> list = new ArrayList<>(); if(hits <=0){ System.out.println("未能召回结果"); return ; }else{ JsonArray items = recallResult.get("result").getAsJsonObject().get("items").getAsJsonArray(); for(JsonElement element:items) { JsonObject object = element.getAsJsonObject(); String splitContent = object.get("fields").getAsJsonObject().get("split_content").getAsString(); list.add(splitContent); } } //3.3调用智能问答版LLM能力进行生成式问答 StringBuffer sb =new StringBuffer(); sb.append("{ \"question\" : \"什么是opensearch\" ,"); sb.append(" \"type\" : \"text\","); sb.append(" \"content\" : ["); for(String str:list){ sb.append("\""); sb.append(str); sb.append("\""); sb.append(","); } sb.deleteCharAt(sb.lastIndexOf(",")); sb.append("]}"); Map<String, String> llmParams = new HashMap<String, String>() {{ put("format", "full_json"); put("_POST_BODY", sb.toString()); }}; System.out.println("llm request params:"+llmParams); String llmPath = String.format("/apps/%s/actions/knowledge-llm", llmAppName); openSearchResult = llmClient.callAndDecodeResult(llmPath, llmParams, "POST"); System.out.println("llm result:"+openSearchResult.getResult()); } }