前沿重器[52] | 聊聊搜索系统5:召回:检索、粗排、多路召回

前沿重器

栏目主要给大家分享各种大厂、顶会的论文和分享,从中抽取关键精华的部分和大家分享,和大家一起把握前沿技术。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有。(算起来,专项启动已经是20年的事了!)

2023年文章合集发布了!在这里:又添十万字-CS的陋室2023年文章合集来袭

往期回顾

  • 前沿重器[47] | RAG开源项目Qanything源码阅读3-在线推理

  • 前沿重器[48] | 聊聊搜索系统1:开篇语

  • 前沿重器[49] | 聊聊搜索系统2:常见架构

  • 前沿重器[50] | 聊聊搜索系统3:文档内容处理

  • 前沿重器[51] | 聊聊搜索系统4:query理解

RAG在整个大模型技术栈里的重要性毋庸置疑,而在RAG中,除了大模型之外,另一个不可或缺的部分,就是搜索系统,大模型的正确、稳定、可控生成,离不开精准可靠的搜索系统,大量的实验中都有发现,在搜索系统足够准确的前提下,大模型的犯错情况会骤然下降,因此,更全面、系统地了解搜索系统将很重要。

听读者建议,像之前的对话系统一样(前沿重器[21-25] | 合集:两万字聊对话系统),我也会拆开揉碎地给大家讲解搜索系统目前业界比较常用的架构、技术方案,目前的计划是分为这几个模块讲解:

  • 开篇语(前沿重器[48] | 聊聊搜索系统1:开篇语):给大家简单介绍一下搜索系统的概况,以及现在大家比较关注在大模型领域的发展情况。

  • 搜索系统的常见架构(前沿重器[49] | 聊聊搜索系统2:常见架构):经过多代人的探索,目前探索出相对成熟可靠,适配多个场景、人力、生产迭代等因素的综合性方案。

  • 文档内容处理(前沿重器[50] | 聊聊搜索系统3:文档内容处理):对原始文档内容、知识的多种处理方案。

  • Query理解(前沿重器[51] | 聊聊搜索系统4:query理解):对query内容进行解析,方便后续检索使用。

  • 召回:检索、粗排、多路召回(本期):使用query理解的结果,从海量数据中找到所需的信息。

  • 精排:对检索的内容进行进一步的精筛,提升返回的准确性。

  • 其他搜索的附加模块:补充说明一些和搜索有关的模块。

本期的内容是检索和多路召回。这3个内容都是围绕着搜索引擎的工作,所以把他们放在一起来说,我就分章节来详细描述吧。

目录:

  • 索引和检索

    • 倒排索引的相关概念

    • 常见索引简介

    • 搜索数据库支持

    • 向量检索DEMO

    • 检索

  • 粗排

  • 展开聊一下向量召回

  • 多路召回

索引

搜索的核心工作是从海量的内容中找到最合适、相关的内容,即有一个“大海捞针”的工作,在“大海”的情况下,逐个匹配的地毯式搜索方案显然并不是,而索引的核心价值,就是加速这个搜索,用尽可能低的复杂度,尤其是时间复杂度,来完成这项工作,甚至可以一定程度牺牲空间复杂度,提到复杂度,后续大家也可以看到有提到大量数据结构方面的知识,要理解这块需要很结实的数据结构基础。

所谓的索引,是指一种存储结构,该存储结构能让检索变得更加快速方便。最早的搜索一般就要从“倒排索引”开始讲起,这是搜索最最基本的技术。此处我也从倒排开始说起。

倒排索引的相关概念

这块内容在(心法利器[96] | 写了个向量检索的baseline)有详细解释,但是因为比较靠后,这里我搬过来。

首先先给大家解释倒排,抛开向量检索,先说字面检索,首先了解为什么我们搜“倒排”,能够出很多有关倒排索引的文章,是因为底层有一套kv结构,和这个就叫做倒排,key是切好的词汇,value是包含这个词汇的所有文档的title,即:

{"倒排":["搜索引擎概述之倒排索引 - 知乎","倒排索引简介","什么是倒排","倒排索引 | Elasticsearch: 权威指南 | Elastic", ...],"搜索":["搜狗搜索","搜索(汉语词语) - 百度百科", ....],"索引":["搜索引擎概述之倒排索引 - 知乎","倒排索引简介","倒排索引 | Elasticsearch: 权威指南 | Elastic", "索引 - 百度百科"...]...
}

我们只需要找到你的检索词,把所有value都给你弄出来,这就叫做查询到了,然而随着库的变大,我们肯定不能把输入的每个字和库里面的做逐一匹配:

query = "倒排"
result = []
for index_key in database:if index_key == query:result.extend(database[index_key])

时间复杂度肯定就有问题(O(n)),不要小看这个线性复杂度,当库里面有千万甚至更多的内容时,线性复杂度也远远不够,我们就要用特定的数据结构来降低检索的时间复杂度,甚至不惜牺牲空间复杂度,对字面的,会考虑trie树等结构,可以把对数据条目数的复杂度降低到常数级,这些结构,我把他叫做索引

至于正排,则是存的对应内容的详情的,例如这个:

[{"title":"搜索引擎概述之倒排索引 - 知乎","docs":"xxxxxxxxxx","insert_time":"2023081315550000"
},{"title":"倒排索引 - 百度文库","docs":"xxxxxxxxxx","insert_time":"2023081316550000"
}]

我们搜的时候,可能是针对title搜的,然而,我们没必要也不可以把别的和查询无关的信息也存到索引中,因此,我们构造了一个额外的数据结构,这样:

{"id1":{"title":"搜索引擎概述之倒排索引 - 知乎","docs":"xxxxxxxxxx","insert_time":"2023081315550000"
},"id2":{"title":"倒排索引 - 百度文库","docs":"xxxxxxxxxx","insert_time":"2023081316550000"
}}

当我们通过倒排查到了id1后,来这个新的数据结构里面,通过id1这个钥匙就能找到这个文档的详情,并且可以展示给用户了,这个结构,就是正排。

好了,这块的科普点到为止,更多有兴趣的内容,可以看《信息检索导论》以及《这就是搜索引擎》这两本书,非常推荐大家看看的。

常见索引简介

上面提到的就是我常说的字面索引,即应对的是字面检索的情况,就是根据用户query内的词汇来进行检索完成,显然字面检索并不能完成我们日常所需,还需要大量的其他索引来支持,这里我举几个其他例子来让大家进一步理解索引多样性的必要性。

大家熟知的向量索引。(心法利器[16] | 向量表征和向量召回)。向量检索应该是大家比较熟悉的方案了,它具有非常强大的语义泛化能力,能让意思比较接近的句子都能被尽快搜索到,这项技术能大大降低我们配置同义词、配置说法的压力,底层常见的方案是hnsw(说起来原理还挺有技术含量的),另外经典的,在《统计学习方法》里,讲KNN的那章,有提到kdtree,当然也有集成的比较好的FAISS方案,有兴趣可以自己了解一下,另外我也是有文章专门给过完整的代码demo的,linux版本推荐(心法利器[96] | 写了个向量检索的baseline向量表征baseline)ngtpy,以及通用的faiss也可以用(心法利器[104] | 基础RAG-向量检索模块(含代码))。

数字索引。细想这么一个query怎么查询——“语文考90分左右的同学”,逐个匹配肯定是很方便的,对海量的数据肯定不合适,向量索引很可能召回的是60分、99分之类可能字面有些接近但是数字范围不太接近的结果,于是便需要数字索引,越接近90分相似度要越高的那种,比较常见的就是BTree系列,这在很多常见的搜索工具中肯定是有集成的。

地理哈希索引。细想这么一个query怎么查询——“故宫附近的美食”,这里依靠的就是地理位置了,常见的方案是GeoHash,通过经纬度可以把位置哈希化,哈希的是根据地理的矩形空间划分的,每一位字符表示的就是特定矩形大小下所属的矩形,因此哈希的字符串越长,代表矩形越小,即表示距离附近。

从这里可以看到,面对不同的检索问题,是需要不同的索引方案的,列举这些是希望大家能打开思路,根据合适的情况进行选择。

搜索数据库支持

尽管索引类型众多,但我们并不需要为此造轮子,目前我还是比较推荐ElasticSearch这个中间件,它具备非常完整的功能,上述提到的索引基本都支持,不支持的也可以通过安装插件的方式来解决。

当然,ElasticSearch比较重,对于数据量较少的,或者功能不需要这么多的,例如只要向量召回,那也没必要用它,Faiss是一个很不错的方案,这个就大家因地制宜吧。

向量检索DEMO

比较推荐大家看我在XXX中提到的Faiss方案,我在这里再展开讲一下吧。项目地址:https://github.com/ZBayes/basic_rag。

项目里和向量检索有关的模块的文件是这些:

`-- src|-- models|   |-- simcse_model.py|   `-- vec_model.py|-- searcher|   |-- searcher.py|   `-- vec_searcher|       |-- vec_index.py|       `-- vec_searcher.py

models里面是向量召回模型,searcher是检索有关的内容。

模型

首先是simcse_model.py,引用我带了链接,用的是一位大佬的模型,方便进行向量化。

import torch
import torch.nn as nn
from loguru import logger
from tqdm import tqdm
from transformers import BertConfig, BertModel, BertTokenizerclass SimcseModel(nn.Module):# https://blog.csdn.net/qq_44193969/article/details/126981581def __init__(self, pretrained_bert_path, pooling="cls") -> None:super(SimcseModel, self).__init__()self.pretrained_bert_path = pretrained_bert_pathself.config = BertConfig.from_pretrained(self.pretrained_bert_path)self.model = BertModel.from_pretrained(self.pretrained_bert_path, config=self.config)self.model.eval()# self.model = Noneself.pooling = poolingdef forward(self, input_ids, attention_mask, token_type_ids):out = self.model(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)if self.pooling == "cls":return out.last_hidden_state[:, 0]if self.pooling == "pooler":return out.pooler_outputif self.pooling == 'last-avg':last = out.last_hidden_state.transpose(1, 2)return torch.avg_pool1d(last, kernel_size=last.shape[-1]).squeeze(-1)if self.pooling == 'first-last-avg':first = out.hidden_states[1].transpose(1, 2)last = out.hidden_states[-1].transpose(1, 2)first_avg = torch.avg_pool1d(first, kernel_size=last.shape[-1]).squeeze(-1)last_avg = torch.avg_pool1d(last, kernel_size=last.shape[-1]).squeeze(-1)avg = torch.cat((first_avg.unsqueeze(1), last_avg.unsqueeze(1)), dim=1)return torch.avg_pool1d(avg.transpose(1, 2), kernel_size=2).squeeze(-1)

然后是model.py,这个旨在包裹模型,并且给出模型预测的一些特定功能,例如推理向量,服务化转化,计算相似度等。

import torch
import torch.nn as nn
import torch.nn.functional as F
from loguru import loggerfrom transformers import BertTokenizerfrom src.models.simcse_model import SimcseModelclass VectorizeModel:def __init__(self, ptm_model_path, device = "cpu") -> None:self.tokenizer = BertTokenizer.from_pretrained(ptm_model_path)self.model = SimcseModel(pretrained_bert_path=ptm_model_path, pooling="cls")self.model.eval()# self.DEVICE = torch.device('cuda' if torch.cuda.is_available() else "cpu")self.DEVICE = deviceself.model.to(self.DEVICE)self.pdist = nn.PairwiseDistance(2)def predict_vec(self,query):q_id = self.tokenizer(query, max_length = 200, truncation=True, padding="max_length", return_tensors='pt')with torch.no_grad():q_id_input_ids = q_id["input_ids"].squeeze(1).to(self.DEVICE)q_id_attention_mask = q_id["attention_mask"].squeeze(1).to(self.DEVICE)q_id_token_type_ids = q_id["token_type_ids"].squeeze(1).to(self.DEVICE)q_id_pred = self.model(q_id_input_ids, q_id_attention_mask, q_id_token_type_ids)return q_id_preddef predict_vec_request(self, query):q_id_pred = self.predict_vec(query)return q_id_pred.cpu().numpy().tolist()def predict_sim(self, q1, q2):q1_v = self.predict_vec(q1)q2_v = self.predict_vec(q2)sim = F.cosine_similarity(q1_v[0], q2_v[0], dim=-1)return sim.numpy().tolist()if __name__ == "__main__":import time,randomfrom tqdm import tqdmvec_model = VectorizeModel('C:/work/tool/huggingface/models/simcse-chinese-roberta-wwm-ext')print(vec_model.predict_vec("什么人不能吃花生"))
检索模块

最内部的是索引,索引外事检索器,首先是索引,这里我把他组件化了。内部支持构建、数据加入、加载、保存、检索等。

import faiss
from loguru import logger
from src.models.vec_model import VectorizeModelclass VecIndex:def __init__(self) -> None:self.index = ""def build(self, index_dim):description = "HNSW64"measure = faiss.METRIC_L2self.index = faiss.index_factory(index_dim, description, measure)def insert(self, vec):self.index.add(vec)def batch_insert(self, vecs):self.index.add(vecs)def load(self, read_path):# read_path: XXX.indexself.index = faiss.read_index(read_path)def save(self, save_path):# save_path: XXX.indexfaiss.write_index(self.index, save_path)def search(self, vec, num):# id, distancereturn self.index.search(vec, num)

外部包一层搜索器,内部可以构造多种索引,根据自己需要调用即可,因为目前只有一个索引,所以从调用函数来看基本是又包了VecIndex一层。

import os, json
from loguru import logger
from src.searcher.vec_searcher.vec_index import VecIndexclass VecSearcher:def __init__(self):self.invert_index = VecIndex() # 检索倒排,使用的是索引是VecIndexself.forward_index = [] # 检索正排,实质上只是个list,通过ID获取对应的内容self.INDEX_FOLDER_PATH_TEMPLATE = "data/index/{}"def build(self, index_dim, index_name):self.index_name = index_nameself.index_folder_path = self.INDEX_FOLDER_PATH_TEMPLATE.format(index_name)if not os.path.exists(self.index_folder_path) or not os.path.isdir(self.index_folder_path):os.mkdir(self.index_folder_path)self.invert_index = VecIndex()self.invert_index.build(index_dim)self.forward_index = []def insert(self, vec, doc):self.invert_index.insert(vec)# self.invert_index.batch_insert(vecs)self.forward_index.append(doc)def save(self):with open(self.index_folder_path + "/forward_index.txt", "w", encoding="utf8") as f:for data in self.forward_index:f.write("{}\n".format(json.dumps(data, ensure_ascii=False)))self.invert_index.save(self.index_folder_path + "/invert_index.faiss")def load(self, index_name):self.index_name = index_nameself.index_folder_path = self.INDEX_FOLDER_PATH_TEMPLATE.format(index_name)self.invert_index = VecIndex()self.invert_index.load(self.index_folder_path + "/invert_index.faiss")self.forward_index = []with open(self.index_folder_path + "/forward_index.txt", encoding="utf8") as f:for line in f:self.forward_index.append(json.loads(line.strip()))def search(self, vecs, nums = 5):search_res = self.invert_index.search(vecs, nums)recall_list = []for idx in range(nums):# recall_list_idx, recall_list_detail, distancerecall_list.append([search_res[1][0][idx], self.forward_index[search_res[1][0][idx]], search_res[0][0][idx]])# recall_list = list(filter(lambda x: x[2] < 100, result))return recall_list

VecSearcher外,还可以有多个检索器,综合起来形成一个简易的搜索工具Searcher

import json,requests,copy
import numpy as np
from loguru import logger
from src.searcher.vec_searcher.vec_searcher import VecSearcher
from src.models.vec_model import VectorizeModelclass Searcher:def __init__(self, model_path, vec_search_path):self.vec_model = VectorizeModel(model_path)logger.info("load vec_model done")self.vec_searcher = VecSearcher()self.vec_searcher.load(vec_search_path)logger.info("load vec_searcher done")def rank(self, query, recall_result):rank_result = []for idx in range(len(recall_result)):new_sim = self.vec_model.predict_sim(query, recall_result[idx][1][0])rank_item = copy.deepcopy(recall_result[idx])rank_item.append(new_sim)rank_result.append(copy.deepcopy(rank_item))rank_result.sort(key=lambda x: x[3], reverse=True)return rank_resultdef search(self, query, nums=3):logger.info("request: {}".format(query))q_vec = self.vec_model.predict_vec(query).cpu().numpy()recall_result = self.vec_searcher.search(q_vec, nums)rank_result = self.rank(query, recall_result)# rank_result = list(filter(lambda x:x[4] > 0.8, rank_result))logger.info("response: {}".format(rank_result))return rank_resultif __name__ == "__main__":VEC_MODEL_PATH = "C:/work/tool/huggingface/models/simcse-chinese-roberta-wwm-ext"VEC_INDEX_DATA = "vec_index_test2023121201"searcher = Searcher(VEC_MODEL_PATH, VEC_INDEX_DATA)q = "什么人不能吃花生"print(searcher.search(q))
灌数据

离线,在进行文档处理后(参考前几周写的这篇文章:前沿重器[50] | 聊聊搜索系统3:文档内容处理),需要把处理好的数据灌入Searcher中,参考这个脚本:

import json,torch,copy
from tqdm import tqdm
from loguru import logger
from multiprocessing import Process,Queue
from multiprocessing import set_start_method
from src.models.vec_model import VectorizeModel
from src.searcher.vec_searcher.vec_searcher import VecSearcher if __name__ == "__main__":# 0. 必要配置VEC_MODEL_PATH = "C:/work/tool/huggingface/models/simcse-chinese-roberta-wwm-ext"SOURCE_INDEX_DATA_PATH = "./data/baike_qa_train.json"VEC_INDEX_DATA = "vec_index_test2023121301_20w"DEVICE = torch.device('cuda' if torch.cuda.is_available() else "cpu")PROCESS_NUM = 2# logger.info("load model done")# 1. 加载数据、模型vec_model = VectorizeModel(VEC_MODEL_PATH, DEVICE)index_dim = len(VectorizeModel(VEC_MODEL_PATH, DEVICE).predict_vec("你好啊")[0])source_index_data = []with open(SOURCE_INDEX_DATA_PATH, encoding="utf8") as f:for line in f:ll = json.loads(line.strip())if len(ll["title"]) >= 2:source_index_data.append([ll["title"], ll])if len(ll["desc"]) >= 2:source_index_data.append([ll["desc"], ll])# if len(source_index_data) > 2000:#     breaklogger.info("load data done: {}".format(len(source_index_data)))# 节省空间,只取前N条source_index_data = source_index_data[:200000]# 2. 创建索引并灌入数据# 2.1 构造索引vec_searcher = VecSearcher()vec_searcher.build(index_dim, VEC_INDEX_DATA)# 2.2 推理向量vectorize_result = []for q in tqdm(source_index_data):vec = vec_model.predict_vec(q[0]).cpu().numpy()tmp_result = copy.deepcopy(q)tmp_result.append(vec)vectorize_result.append(copy.deepcopy(tmp_result))# 2.3 开始存入for idx in tqdm(range(len(vectorize_result))):vec_searcher.insert(vectorize_result[idx][2], vectorize_result[idx][:2])# 3. 保存vec_searcher.save()

检索

所谓的检索,就是把内容从数据库里搜出来,这里介绍两个吧,一个是从elasticsearch(后称为ES)中把数据搜索出来,另一个是从上面我写的组件里搜出来。

ES在python有专门的客户端,配合客户端和专用的检索语句DSL,具体的逻辑参考:https://blog.csdn.net/CSDN_of_ding/article/details/131761666,这个和mysql的链接使用是类似的,难度不是很高。难度主要在检索语法的设计,因为ES主要是字面的检索,可能会有一些复杂的逻辑,与、或、非还有一些打分逻辑啥的,这块功能做得很灵活,具体的可以参考权威指南:https://www.elastic.co/guide/cn/elasticsearch/guide/current/getting-started.html。

至于上述写的组件,则比较简单,就是直接一个语句就好了。Search里面内置了对应的模型,内部已经把向量化和搜索都已经完成了。

if __name__ == "__main__":VEC_MODEL_PATH = "C:/work/tool/huggingface/models/simcse-chinese-roberta-wwm-ext"VEC_INDEX_DATA = "vec_index_test2023121201"searcher = Searcher(VEC_MODEL_PATH, VEC_INDEX_DATA)q = "什么人不能吃花生"print(searcher.search(q))

内部的逻辑可以重看一下这个函数,内部的逻辑基本就是3个过程——向量化、检索、排序。

def search(self, query, nums=3):logger.info("request: {}".format(query))q_vec = self.vec_model.predict_vec(query).cpu().numpy()recall_result = self.vec_searcher.search(q_vec, nums)rank_result = self.rank(query, recall_result)# rank_result = list(filter(lambda x:x[4] > 0.8, rank_result))logger.info("response: {}".format(rank_result))return rank_result

粗排

在前沿重器[49] | 聊聊搜索系统2:常见架构一文中,我有对粗排、精排这些排序的模块,进行过详细的分析和解释,让大家理解,为什么要划分精排和粗排,甚至是多阶段的划分。此处,我单独把粗排拎了出来,它的核心工作是对本路内容进行一个粗略的相似度排序,毕竟检索的目的是找到最接近的TOPN,这里不可绕开的要衡量“最接近“。

粗排一定程度和检索逻辑绑定,其本质任务就是计算query和doc对应检索字段之间的相似度,利用相似度的数值,可以进行排序筛选和过滤。这里有亮点需要强调:

  • 粗排的核心目标是干掉“肯定不合适”的结果,所以常常要考虑的是“相似”or“不相似”的问题,作为对比,精排由于进入精排层的物料多半是和原query比较接近,此时的粗排的分数一般都会比较接近,此时精排任务已经变成对比哪个物料“更相似”,要求一个更能拉开物料之间分数差异的算法。

下面举几个用于粗排的相似度计算方法。

  • 如果是字面召回,我们重点关注的是字面的相似度,常见的是BM25,目前已经是非常普及的方案了,当然还有我之前有提到的cqr/ctr(心法利器[18] | cqr&ctr:文本匹配的破城长矛、心法利器[99] | 无监督字面相似度cqr/ctr源码),因为BM25的数值受到句子长度影响很明显,所以并不容易卡阈值,后者cqr/ctr方案则可以很好地处理这个一点,后者一般可以作为配合前者的存在。

  • 一般的向量召回则更多考虑cos、L2之类常用的距离。

  • 如果是数字等方面的召回,直接算误差即可。

  • 如果是地理位置,可以通过经纬度很容易计算到直线距离,有地图功能时甚至可以算出导航距离。

展开聊一下向量召回

向量召回在目前之所以得到流行,除了目前已经流传已久的泛化性的原因,还有一个是灵活性。只需要根据一个目标,把内容转化为向量,即可用于进行向量召回,不需要考虑各种索引的处理,从前文大家也知道不同索引要处理的事还挺麻烦的。

首先,需要强调的第一个问题,从表征目标出发,就是向量不止有语义向量,向量表征对标的内容可以是非常丰富的,在推荐系统中,类似协同过滤的设计,是可以转为向量来做的,再者在搜索领域,也有像淘宝(前沿重器[18] | KDD21-淘宝向量检索)讲用户行为偏好转为向量的案例,就是常规意义的搜索,也可以通过query-用户点击的方式,结合内容来源、内容质量等特征,构造对比学习来学习向量的方式,因此大家可以考虑把思路打开,语义向量检索只是向量的一部分。

第二,是有关向量的特征,除了文本可以转化,还有其他特征,这点可以从大家比较熟知的推荐系统中借鉴,如果存在个性化信息,用户的行为、偏好、年龄、地点之类的是可以作为表征的输入的,另外物料侧,除了考虑多种类型的文本,如问题、回答、标题的常规已有特征外,还有内容质量、内容用户画像、话题标签等特征,内容质量可以是用户平均停留时间、点击率之类的,内容用户画像则是对表达喜欢的用户的特征进行表征,另外话题是可以通过内容理解来抽取,这些特征都非常有利于进行召回。

多路召回

多路召回是搜索里面很常见的操作。因为用户提问的复杂性、内容的多样性等原因,我们往往不会一路把所有内容都召回回来,如何分路成为一个值得探讨的问题,下面我会分几个维度来讲多路召回可能的操作,并会提及具体的使用场景,供大家参考使用。

  • 意图/路由划分下的多路召回。针对不同的需求,可能需要不同的操作来满足,例如要查音乐和查天气,后续的操作会不同,搜音乐以来音乐库,查天气则是查询天气接口,此时显然就是要走不同的链路来进行内容召回,再者同样是音乐类,用户可能是按歌手、歌名、流派等因素查,复杂以后很可能需要多路召回来实现,另外也需要配合上游的意图识别、实体抽取等因素,来切分链路,然后根据链路来进行召回。

  • 不同内容结构下的多路召回。这个比较简单,举例,音乐和购物,背后的数据结构是不同的,但用户的说法可能是模糊的,例如说一个专辑名,可能是要买专辑,也可能是想听音乐,此时多路召回是一个不错的选择,不着急直接看哪个优先级高,这事可以放精排层来做。

  • 不同检索方式下的多路召回。这个也比较简单,对不同的检索方案,不好放一起的,分两路是不错的选择(当然也有不分的方法),例如向量召回和字面召回两路。

  • 不同表征方式的多路召回。上面有提到向量召回的多样性,基于表征目标和表征特征是可以有多种向量表征方式的,基于不同的表征特征,就可以有多种不一样的召回方式。

提醒,多种召回方式,在一般情况下都是并发进行的,毕竟他们运行需要一定的时间,而且一般互不影响,一般就是服务化后用多进程请求的方式来进行。

小结

本文讨论了搜索系统中召回层的操作,重点聚焦在索引、粗排、向量召回、多路召回等工作中,供大家更深入全面理解搜索系统的召回部分。另外,仍旧建议大家多去翻翻《信息检索导论》,虽然现在视角里面的内容算是旧的,但时至今日仍有大量知识会用到,大家可以系统学习。

0bfd059383230088b0e8cd25eb1436d4.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/38546.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows定时任务执行脚本

场景&#xff1a;由于网络波动原因导致云数据库没连接上&#xff0c;从而导致某个流程引擎链接不上数据库从而导致该流程引擎服务挂了&#xff0c;网络恢复后 数据库链接正常&#xff0c;但是该引擎服务还是中止状态。 解决方案&#xff1a;在Windows中新建一个定时任务&#…

为用户转出并处理MODIS NDVI数据案例过程记述,希望对大家有用!

最近为用户转出和处理了一次MODIS NDVI数据&#xff0c;我将过程做了个总结供大家参考&#xff01;希望能帮助到一些下载和处理数据的朋友! 使用工具为三个&#xff1a;地图资源工具 和 GIS数据转换器-栅格&#xff0c;qgis。 1.选择【数据下载功能】&#xff0c;然后选择MO…

复制 pdf 的表格到 markdown 版本的Typora 或者 word 中

在 pdf 中选中复制表格内容&#xff0c;直接粘贴到 typora 中失败&#xff0c;可以使用 txt文件和 excel 做过渡。 准备一个空的 txt 文件&#xff0c;将 pdf 中表格的数据复制粘贴到txt文件中&#xff0c;文本内容会以空格分开&#xff0c;如下图的形式&#xff1a; 打开 exc…

firewalld防火墙转发流量到其他端口forward port rules

假设云主机eth0: 47.93.27.106 tun0: inet 10.8.0.1 netmask 255.255.255.0 Show rules for a specific zone (public) sudo firewall-cmd --zonepublic --list-all Add the tun0 interface to the public zone: sudo firewall-cmd --zonepublic --add-interfacetun0 --…

手把手教你考下39张免费亚马逊AWS证书和学习徽章

小李哥目前共考了39项亚马逊云(AWS)徽章&#xff0c;这也是普通用户可考的全部徽章。这篇文章会介绍如何报名、复习、通过这39张徽章提升云计算基本技能&#xff0c;了解全球第一大云厂亚马逊云科技前沿技术。这篇文章在领英爆&#x1f525;&#xff0c;有将近100k浏览量和11k的…

MeterSphere v3.0全新启航,让软件测试工作更简单、更高效

2024年7月1日&#xff0c;MeterSphere v3.0版本正式发布。MeterSphere v3.0是新一代的测试管理和接口测试工具&#xff0c;致力于让软件测试工作更简单、更高效&#xff0c;不再成为持续交付的瓶颈。 在团队协作方面&#xff0c;针对目前企业软件测试团队所面临的测试工具不统…

深度学习项目GPU开发环境安装

注安装环境&#xff1a;ubuntu22.04, cuda 11.7, cudnn8.9 1.安装nvidia驱动 看可安装的Nvidia驱动版本&#xff1a;执行 ubuntu-drivers devices 安装合适版本的Nvidia驱动&#xff1a; sudo apt-get install nvidia-driver-515 注意&#xff1a;合适的版本需要尝试&#x…

从0开始建SMARTFORMS表格

一、简介步骤 1、设置纸张的大小&#xff08;页格式&#xff09; 2、设置字体大小&#xff08;样式&#xff09; 3、设置表格模板 二、详细操作步骤 1、设置页格式 事务码&#xff1a;SPAD 参考操作&#xff1a;SAP Smartforms页格式创建与使用_sap 页格式-CSDN博客 SA…

【网络安全】修改Host文件实现域名解析

场景 开发一个网站或者服务&#xff0c;需要在本地测试时&#xff0c;可以将线上的域名指向本地开发环境的IP地址。从而模拟真实环境中的域名访问&#xff0c;方便调试和开发。 步骤 1、以管理员身份打开命令提示符 2、编辑hosts文件&#xff1a; 输入以下命令打开hosts文…

【第六节】C/C++静态查找算法

目录 前言 一、搜索查找 二、查找算法 1. 线性查找&#xff08;Linear Search&#xff09; 2. 二分查找&#xff08;Binary Search&#xff09; 3. 插值查找&#xff08;Interpolation Search&#xff09; 4. 哈希查找&#xff08;Hash Search&#xff09; 5. Fibonacc…

C++感受12-Hello Object 派生版

不变的功能&#xff0c;希望直接复用原有代码&#xff1b;变化的功能&#xff0c;希望在分开的代码里实现。 派生的基本概念和目的如何定义派生类以及创建派生对象派生对象的生死过程 0. 课堂视频 ff14-HelloObject-派生版 1. 派生的基本概念与目的 编程&#xff0c;或者说软…

vue中的坑·

常规 1.使用watch时&#xff0c;immediate true会在dom挂载前执行 2.使用this.$attrs和props 可以获取上层非原生属性&#xff08;class/id&#xff09; 多层次嵌套引用 设置的时候直接赋值&#xff0c;修改的时候即使用的双向绑定加上$set / nextick / fouceUpdate都不会同步…

HiBit Uninstaller:软件批量卸载,一触即得

名人说&#xff1a;莫道谗言如浪深&#xff0c;莫言迁客似沙沉。 ——刘禹锡《浪淘沙》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、软件介绍1、HiBit Uninstaller2、核心功能 二、下载安装1、下载2、安装 …

山东省安管人员考核报名流程及免冠证件照处理方法

随着《交通运输工程施工单位主要负责人、项目负责人和专职安全生产管理人员安全生产考核管理办法》&#xff08;以下简称《办法》&#xff09;的发布&#xff0c;山东省的安管人员迎来了新的考核要求。本文将为您详细解读山东省安管人员考核的报名流程&#xff0c;并提供免冠证…

【MotionCap】搭建wsl2的pytorch环境

参考大神:wsl2-ubuntu版本 cuda下周cuda11.3 wget https://developer.download.nvidia.com/compute/cuda/11.3.0/local_installers/cuda_11.3.0_465.19.01_linux.run sudo sh cuda_11.3.0_465.19.01_linux.run cuda是开源的么?下15分钟

1、什么是SSD?

概念 SSD&#xff08;Solid State Drive&#xff09;固态硬盘&#xff0c;是以闪存为介质的存储设备&#xff1b;这里突出的重点是闪存。 闪存&#xff0c;也就是常说的flash&#xff0c;分为NOR 和 NAND&#xff1b; NOR的地址线和数据线分开&#xff0c;所以NOR芯片可以像…

“一带一路”再奏强音!秘鲁总统博鲁阿尔特参访苏州金龙

6月27日下午&#xff0c;首次访华的秘鲁共和国总统博鲁阿尔特一行到苏州金龙参观访问&#xff0c;受到了苏州金龙总经理黄书平的热情接待。 黄书平&#xff08;左二&#xff09;向博鲁阿尔特&#xff08;右一&#xff09;介绍苏州金龙发展情况 从苏州金龙发展历程、产品技术研…

使用Nginx反向代理KKFileView遇到问题

使用KKFileView 4.0 以上版本 在KKFileView官网上&#xff0c;关于使用Nginx代理&#xff0c;建议配置如下 一、修改Nacos 在Nginx的conf文件夹中修改 nginx.conf ,新加 红框内的IP地址为代理服务器地址&#xff08;即安装KKFileView的服务器地址&#xff09; 二、修改KKFil…

小程序打包

一、manifest.json文件添加小程序id 二、接口校验&#xff0c;后端接口添加正式上线&#xff0c;有域名的地址 然后到微信公众平台-开发管理-服务器域名处配置request合法域名&#xff0c;在 此处能够看到后端的baseUrl 三、项目部署 四、发版 在小程序编辑器里 此处可以在…

Android Studio 2023版本切换DNK版本

选择自己需要的版本下载 根目录下的配置路劲注意切换 build.gradle文件下的ndkVersion也要配好对应版本