增强常见问题解答搜索引擎:在 Elasticsearch 中利用 KNN 的力量

在快速准确的信息检索至关重要的时代,开发强大的搜索引擎至关重要。 随着大型语言模型和信息检索架构(如 RAG)的出现,在现代软件系统中利用文本表示(向量/嵌入)和向量数据库已变得越来越流行。 在本文中,我们深入研究了如何使用 Elasticsearch 的 K 最近邻 (KNN) 搜索和来自强大语言模型的文本嵌入,这是一个强大的组合,有望彻底改变我们访问常见问题 (FAQ) 的方式。 通过对 Elasticsearch 的 KNN 功能的全面探索,我们将揭示这种集成如何使我们能够创建尖端的常见问题解答搜索引擎,通过以闪电般的延迟理解查询的语义上下文,从而增强用户体验。

在开始设计解决方案之前,让我们了解信息检索系统中的一些基本概念。

文本表示(嵌入)

你可以通过阅读 “Elasticsearch:什么是向量和向量存储数据库,我们为什么关心?” 来了解更多的关于文本嵌入的知识。

嵌入是一条信息的数字表示,例如文本、文档、图像、音频等。 该表示捕获了所嵌入内容的语义,使其对于许多行业应用程序来说都是稳健的。

语义搜索

传统的搜索系统使用词法匹配来检索给定查询的文档。 语义搜索旨在使用文本表示(嵌入)来理解查询的上下文,以提高搜索准确性。

语义搜索的类型

  • 对称语义搜索:查询和搜索文本长度相似的搜索用例。 例如 在数据集中找到类似的问题。
  • 非对称语义搜索:查询和搜索文本长度不同的搜索用例。 例如 查找给定查询的相关段落。

向量搜索引擎(向量数据库)

向量搜索引擎是专用数据库,可用于将图像、文本、音频或视频等非结构化信息存储为嵌入或向量。 在本文中,我们将使用 Elasticsearch 的向量搜索功能。

现在我们了解了搜索系统的构建块,让我们深入了解解决方案架构和实现。

  1. 搜索解决方案的第一步是将问题-答案对索引到 Elasticsearch 中。 我们将创建一个索引并将问题和答案嵌入存储在同一索引中。 我们将根据检索的特征使用两个独立的模型来嵌入问题和答案。
  2. 我们将使用步骤 1 中使用的相同模型来嵌入查询,并形成搜索查询(3 个部分,即问题、答案、词汇搜索),将查询嵌入映射到相应的问题和答案嵌入。
  3. 我们还将为查询的每个部分提供一个提升值,以表示它们在组合中的重要性。 返回的最终结果根据分数总和乘以各自的提升值进行排名。

环境设置

要使用 docker 安装 Elasticsearch,请参阅这篇有关如何设置单节点集群的详细文章。 如果你已有集群,请跳过此步骤。如果你想详细了解如何安装 Elasticsearch,请参考文章 “如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch”。在本演示中,我们将使用 Elastic Stack 8.10.4 来进行展示。

设置你的索引。 你可以使用以下映射作为起点。我们在 Kibana 的 Dev Tools 中打入如下的命令:

PUT faq-index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "Question": {
        "type": "text"
      },
      "Answer": {
        "type": "text"
      },
      "question_emb": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "dot_product"
      },
      "answer_emb": {
        "type": "dense_vector",
        "dims": 1024,
        "index": true,
        "similarity": "dot_product"
      }
    }
  }
}

模型选择

由于我们使用相当通用的语言处理数据,因此为了进行本实验,我从 MTEB 排行榜的检索(用于答案)和 STS(用于问题)部分中选择了表现最好的模型。

选定型号:

  1. 答案:BAAI/bge-large-en-v1.5(您可以使用量化版本以加快推理速度)
  2. 如有问题:thenlper/gte-base

如果你有特定领域的常见问题解答并想要检查哪种模型表现最好,你可以使用 Beir。 查看本节,其中描述了如何加载自定义数据集以进行评估。

实现

出于本实验的目的,我将使用 Kaggle 的心理健康常见问题解答数据集。

安装所需要的模块

pips install sentence_transformers

1. 装载数据

import pandas as pd
data = pd.read_csv('Mental_Health_FAQ.csv')

2. 生成嵌入

Questions

from sentence_transformers import SentenceTransformer
question_emb_model = SentenceTransformer('thenlper/gte-base')

data['question_emb'] = data['Questions'].apply(lambda x: question_emb_model.encode(x, normalize_embeddings=True))

注意:我们对嵌入进行归一化,以使用点积作为相似性度量而不是余弦相似性。 该计算速度更快,并且在 Elasticsearch 密集向量场文档中得到推荐。

Answers:

answer_emb_model = SentenceTransformer('BAAI/bge-large-en-v1.5')
data['answer_emb'] = data['Answers'].apply(lambda x: answer_emb_model.encode(x, normalize_embeddings=True))

3. 索引文档

我们将使用 Elasticsearch  helper 函数。 具体来说,我们将使用 streaming_bulk API 来索引我们的文档。

首先,让我们实例化 elasticsearch python 客户端。

我们首先需要把安装好的 Elasticsearch 的证书拷贝到当前目录中:

$ pwd
/Users/liuxg/python/faq
$ cp ~/elastic/elasticsearch-8.10.4/config/certs/http_ca.crt .
$ ls
Mental Health FAQ.ipynb archive (13).zip
Mental_Health_FAQ.csv   http_ca.crt

然后我们打入如下的代码:

from elasticsearch import Elasticsearch

from ssl import create_default_context

context = create_default_context(cafile=r"./http_ca.crt")
es = Elasticsearch('https://localhost:9200',
    basic_auth=('elastic', 'YlGXk9PCN7AUlc*VMtQj'),
    ssl_context=context,
)

接下来,我们需要创建一个可以输入到流式 bulk API 中的文档生成器。

index_name="faq-index"
def generate_docs():
    for index, row in data.iterrows():
        doc = {
                "_index": index_name,
                "_source": {
                    "faq_id":row['Question_ID'],
                    "question":row['Questions'],
                    "answer":row['Answers'],
                    "question_emb": row['question_emb'],
                    "answer_emb": row['answer_emb']
                },
            }

        yield doc

最后,我们可以索引文档。

import tqdm
from elasticsearch.helpers import streaming_bulk
number_of_docs=len(data)
progress = tqdm.tqdm(unit="docs", total=number_of_docs)
successes = 0
for ok, action in streaming_bulk(client=es, index=index_name, actions=generate_docs()):
    progress.update(1)
    successes += ok

print("Indexed %d/%d documents" % (successes, number_of_docs))

4. 查询文档

def faq_search(query="", k=10, num_candidates=10):
    
    if query is not None and len(query) == 0:
        print('Query cannot be empty')
        return None
    else:
        query_question_emb = question_emb_model.encode(query, normalize_embeddings=True)

        instruction="Represent this sentence for searching relevant passages: "

        query_answer_emb = answer_emb_model.encode(instruction + query, normalize_embeddings=True)

        payload = {
          "query": {
            "match": {
              "title": {
                "query": query,
                "boost": 0.2
              }
            }
          },
          "knn": [ {
            "field": "question_emb",
            "query_vector": query_question_emb,
            "k": k,
            "num_candidates": num_candidates,
            "boost": 0.3
          },
          {
            "field": "answer_emb",
            "query_vector": query_answer_emb,
            "k": k,
            "num_candidates": num_candidates,
            "boost": 0.5
          }],
          "size": 10,
          "_source":["faq_id","question", "answer"]
        }

        response = es.search(index=index_name, body=payload)['hits']['hits']

        return response

按照模型页面上的说明,我们需要在将查询转换为嵌入之前将指令附加到查询中。 此外,我们使用模型的 v1.5,因为它具有更好的相似度分布。 查看型号页面上的常见问题解答以了解更多详细信息。

评估

为了了解所提出的方法是否有效,根据传统的 KNN 搜索系统对其进行评估非常重要。 让我们尝试定义这两个系统并评估所提出的系统。

  • 系统 1:非对称 KNN 搜索(查询和答案向量)。
  • 系统2:查询(BM25)、非对称KNN搜索(查询和答案向量)和对称KNN搜索(查询和问题向量)的组合。

为了评估系统,我们必须模仿用户如何使用搜索。 简而言之,我们需要从源问题生成与问题复杂性相似的释义问题。 我们将使用 t5-small-finetuned-quora-for-paraphrasing 微调模型来解释问题。

让我们定义一个可以生成释义问题的函数。

from transformers import AutoModelWithLMHead, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("mrm8488/t5-small-finetuned-quora-for-paraphrasing")
model = AutoModelWithLMHead.from_pretrained("mrm8488/t5-small-finetuned-quora-for-paraphrasing")

def paraphrase(question, number_of_questions=3, max_length=128):
    input_ids = tokenizer.encode(question, return_tensors="pt", add_special_tokens=True)

    generated_ids = model.generate(input_ids=input_ids, num_return_sequences=number_of_questions, num_beams=5, max_length=max_length, no_repeat_ngram_size=2, repetition_penalty=3.5, length_penalty=1.0, early_stopping=True)

    preds = [tokenizer.decode(g, skip_special_tokens=True, clean_up_tokenization_spaces=True) for g in generated_ids]

    return preds

现在我们已经准备好了释义函数,让我们创建一个评估数据集,用于测量系统的准确性。

temp_data = data[['Question_ID','Questions']]

eval_data = []

for index, row in temp_data.iterrows():
    preds = paraphrase("paraphrase: {}".format(row['Questions']))
    
    for pred in preds:
        temp={}
        temp['Question'] = pred
        temp['FAQ_ID'] = row['Question_ID']
        eval_data.append(temp)
    
eval_data = pd.DataFrame(eval_data)

#shuffle the evaluation dataset
eval_data=eval_data.sample(frac=1).reset_index(drop=True)

上面的代码生成相应的测试 Question,它们的结果如下:

最后,我们将修改 “faq_search” 函数以返回各个系统的 faq_id。

对于系统 1:

def get_faq_id_s1(query="", k=5, num_candidates=10):
    
    if query is not None and len(query) == 0:
        print('Query cannot be empty')
        return None
    else:
        instruction="Represent this sentence for searching relevant passages: "

        query_answer_emb = answer_emb_model.encode(instruction + query, normalize_embeddings=True)

        payload = {
          "knn": [
          {
            "field": "answer_emb",
            "query_vector": query_answer_emb,
            "k": k,
            "num_candidates": num_candidates,
          }],
          "size": 1,
          "_source":["faq_id"]
        }

        response = es.search(index=index_name, body=payload)['hits']['hits']

        return response[0]['_source']['faq_id']

对于系统 2:

def get_faq_id_s2(query="", k=5, num_candidates=10):
    
    if query is not None and len(query) == 0:
        print('Query cannot be empty')
        return None
    else:
        query_question_emb = question_emb_model.encode(query, normalize_embeddings=True)

        instruction="Represent this sentence for searching relevant passages: "

        query_answer_emb = answer_emb_model.encode(instruction + query, normalize_embeddings=True)

        payload = {
          "query": {
            "match": {
              "title": {
                "query": query,
                "boost": 0.2
              }
            }
          },
          "knn": [ {
            "field": "question_emb",
            "query_vector": query_question_emb,
            "k": k,
            "num_candidates": num_candidates,
            "boost": 0.3
          },
          {
            "field": "answer_emb",
            "query_vector": query_answer_emb,
            "k": k,
            "num_candidates": num_candidates,
            "boost": 0.5
          }],
          "size": 1,
          "_source":["faq_id"]
        }

        response = es.search(index=index_name, body=payload)['hits']['hits']

        return response[0]['_source']['faq_id']

注意:boost 值是实验性的。 为了这个实验的目的,我根据组合中各个字段的重要性进行了划分。 搜索中每个字段的重要性完全是主观的,可能由业务本身定义,但如果不是,系统的一般经验法则是 Answer 向量 > Question 向量 > 查询。

好的! 我们一切准备就绪,开始我们的评估。 我们将为两个系统生成一个预测列,并将其与原始 faq_id 进行比较。

eval_data['PRED_FAQ_ID_S1'] = eval_data['Question'].apply(get_faq_id_s1)

from sklearn.metrics import accuracy_score

ground_truth = eval_data["FAQ_ID"].values
predictions_s1 = eval_data["PRED_FAQ_ID_S1"].values

s1_accuracy = accuracy_score(ground_truth, predictions_s1)

print('System 1 Accuracy: {}'.format(s1_accuracy))

eval_data['PRED_FAQ_ID_S2'] = eval_data['Question'].apply(get_faq_id_s2)

predictions_s2 = eval_data["PRED_FAQ_ID_S2"].values

s2_accuracy = accuracy_score(ground_truth, predictions_s2)

print('System 2 Accuracy: {}'.format(s2_accuracy))

通过所提出的系统,我们可以看到与非对称 KNN 搜索相比,准确率提高了 7-11%。

我们还可以尝试 ramsrigouthamg/t5_paraphraser,但该模型生成的问题有点复杂和冗长(尽管在上下文中)。

你还可以使用 LLM 生成评估数据集并检查系统的性能。

准确性的提高是主观的,取决于查询的质量,即 查询的上下文有多丰富、嵌入的质量和/或使用搜索的用户类型。 为了更好地理解这一点,让我们考虑两种最终用户:

  1. 想要了解有关您的产品和服务的一些事实的一般用户:在这种情况下,上述系统会做得很好,因为问题简单、直观且上下文充分。
  2. 领域/产品特定用户,例如 想要了解产品的一些复杂细节以设置系统或解决某些问题的工程师:在这种情况下,查询在词汇组成方面更具特定于领域,因此开箱即用的模型嵌入将无法捕获所有上下文。 那么,我们该如何解决这个问题呢? 系统的架构将保持不变,但可以通过使用特定领域数据(或预先训练的特定领域模型)微调这些模型来提高搜索系统的整体准确性。

结论

在本文中,我们提出并实现了结合搜索类型的常见问题解答搜索。 我们研究了 Elasticsearch 如何使我们能够结合对称和非对称语义搜索,从而将搜索系统的性能提高高达 11%。 我们还了解所提出的搜索架构的系统和资源要求,这将是考虑采用这种方法时的主要决定因素。

你可以在我的 Github 存储库中找到源笔记本。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

javaweb+mysql的电子书查阅和下载系统

图书分类查看、热门下载、最新上传、站内数据统计。 登陆注册、图书查询、图书详情、图书下载。 身份分为管理员和用户。 源码下载地址 支持:远程部署/安装/调试、讲解、二次开发/修改/定制

串口占用检测工具

串口占用检测工具 平时需要检测哪个程序占用了串口,下面介绍一款非常方便的工具,它的工具箱里包含一个串口占用检测工具,可以非常方便的检测出来哪个程序占用了串口,并给出程序名和PID。 官网下载地址:http://www.red…

安装 tensorflow==1.15.2 遇见的问题

一、直接安装 命令:pip install tensorflow1.15.2 二、换 阿里云 镜像源 命令:pip install -i http://mirrors.aliyun.com/pypi/simple tensorflow1.15.2 三、换 豆瓣 镜像源 命令:pip install http://pypi.douban.com/simple tensorflow1…

UWB室内定位系统全套源码 高精度人员定位系统源码

UWB室内定位系统全套源码 高精度人员定位系统源码 UWB室内定位系统是一种高精度的室内定位技术,它可以实现对室内人员和物品的实时精确定位,具有重要的应用意义和社会价值。 UWB定位精度在厘米级内,其精度远远高于WIFI和蓝牙定位。精度、安全…

华为eNSP配置专题-策略路由的配置

文章目录 华为eNSP配置专题-策略路由的配置0、概要介绍1、前置环境1.1、宿主机1.2、eNSP模拟器 2、基本环境搭建2.1、终端构成和连接2.2、终端的基本配置 3、配置接入交换机上的VLAN4、配置核心交换机为网关和DHCP服务器5、配置核心交换机和出口路由器互通6、配置PC和出口路由器…

ubuntu安装nps客户端

Ubuntu安装nps客户端 1.什么是nps内网穿透?2.设备情况3.下载客户端3.链接服务端3.1、无配置文件模式3.2、注册到系统服务(启动启动、监控进程) 1.什么是nps内网穿透? nps是一款轻量级、高性能、功能强大的内网穿透代理服务器。目前支持tcp、udp流量转发…

单片机为什么一直用C语言,不用其他编程语言?

单片机为什么一直用C语言,不用其他编程语言? 51 单片机规模小得拮据,C 的优势几乎看不到。放个类型信息进去都费劲,你还想用虚函数?还想模板展开?程序轻松破 10k。最近很多小伙伴找我,说想要一些…

uview 1 uni-app表单 number digit 的输入框有初始化赋值后,但是校验失败

背景: 在onReady初始化规则 onReady() { this.$refs.uForm.setRules(this.rules); }, 同时:ref,model,rules,props都要配置好。 报错 当input框限定type为number,digit类型有初始值不做修改动作,直接提交会报错,验…

leetCode 76. 最小覆盖子串 + 滑动窗口 + 哈希Hash

我的往期文章:此题的其他解法,感兴趣的话可以移步看一下: leetCode 76. 最小覆盖子串 滑动窗口 图解(详细)-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/134042115?spm1001.2014.3001.5501 力…

Java SE 学习笔记(十四)—— IO流(2)

目录 1 字节流1.1 字节流写数据1.1.1 创建字节输出流对象1.1.2 字节流写数据 1.2 字节流读数据1.2.1 创建字节输入流对象1.2.2 字节流读数据 1.3 字节流复制文件1.4 流的刷新与关闭1.5 资源释放方式1.5.1 try-catch-finally1.5.2 try-with-resource 2 字符流2.1 字符流概述2.2 …

PyCharm中文使用详解

PyCharm是一个Python IDE,可以帮助程序员节省时间,提高生产力。那么具体怎么用呢?本文介绍了PyCharm的安装、插件、外部工具、专业功能等,希望对大家有所帮助。 之前没有系统介绍过PyCharm。如何配置环境,如何DeBug&a…

springBoot与Vue共同搭建webSocket环境

欢迎使用Markdown编辑器 你好! 这片文章将教会你从后端springCloud到前端VueEleementAdmin如何搭建Websocket 前端 1. 创建websocket的配置文件在utils文件夹下websocket.js // my-js-file.js import { Notification } from element-ui // 暴露自定义websocket对…

MSQL系列(九) Mysql实战-Join算法底层原理

Mysql实战-Join算法底层原理 前面我们讲解了BTree的索引结构,及Mysql的存储引擎MyISAM和InnoDB,今天我们来详细讲解下Mysql的查询连接Join的算法原理 文章目录 Mysql实战-Join算法底层原理1.Simple Nested-Loop Join 简单嵌套循环2.Block Nested-Loop Join 块嵌套…

linux 内存检测工具 kfence 详解(一)

版本基于: Linux-5.10 约定: PAGE_SIZE:4K 内存架构:UMA 系列博文: linux 内存检测工具 kfence 详解(一) linux 内存检测工具 kfence 详解(二) 0. 前言 本文 kfence 之外的代码版本是基于 Linux5.10,…

ORACLE-递归查询、树操作

1. 数据准备 -- 测试数据准备 DROP TABLE untifa_test;CREATE TABLE untifa_test(child_id NUMBER(10) NOT NULL, --子idtitle VARCHAR2(50), --标题relation_type VARCHAR(10) --关系,parent_id NUMBER(10) --父id );insert into untifa_test (CHILD_ID, TITLE, RELATION_TYP…

React 核心与实战2023版

课程亮点: 完整的前后台项目(PC+移动;完成业务;)React 最新企业标准技术栈(React 18 + Redux + ReactRouter + AntD)React + TypeScript (为大型项目奠定了基础)课程内容安排: React 介绍 React 是什么? React 是由Meta公司研发,是一个用于 构建Web和原生交互界面…

支持CT、MR三维后处理的医学PACS源码

医学影像归档与通信系统(picture archiving and communication systems,PACS)是应用于医院的数字医疗设备,如CT、MR(磁共振)、US(超声成像)、X线、DSA(数字减影&#xff…

npm更新包时This operation requires a one-time password.

[访问我的npm包](mhfwork/yt-ui - npm) 更新npm包时出现 This operation requires a one-time password.是因为需要认证 解决办法 1. 点击红线处的链接 2. 进入npm官网获取指定秘钥 3. 再次填入 one-time password 即可

word页脚设置,页脚显示第几页共有几页设置步骤

word页脚设置,页脚显示第几页共有几页设置步骤: 具体步骤: 步骤1: 步骤1.1选择页脚---空白页脚 步骤1.2,在"[在此处键入]",直接输入你需要的格式,如 “第页/共页” 步骤1.3选择第“…

定义USB接口,鼠标类和键盘类都可以作为实现类去实现USB接口

目录 程序设计 程序分析 系列文章 ​ 如图所示,我们电脑上都有USB接口,当我们的鼠标和键盘插上去之后才可以使用,拔出来就关闭使用。其实具体是什么USB设备,笔记本并不关心,只要符合USB规格的设备都可以。鼠标和键盘要想能在电脑上使用,那么鼠标和键盘也必须遵守USB规范…