向量召回 ES

应用场景

Elasticsearch支持词向量搜索能够在很多场景下进行应用,这里进行列举一些简单的应用,有些并不是当前场景下的最佳选择。

  1. QA:用户输入一段描述,给出最佳匹配的答案。传统基于关键字搜索问答的局限性之一在于用户必须了解一些特殊的名词,假如关键字没有匹配上则没有返回结果。而在使用词向量之后,直接输入类似的描述性语言可以获得最佳匹配的答案。
  2. 文章搜索:有时候只记得一篇文章在表达什么意思,而忘记了文章标题和关键字。这时候只需要输入自己记得的大致意思和记得句子,即可根据描述中隐藏的语义信息搜索到最佳匹配的文章。
  3. 图片搜索:这里的图片搜索有两种含义,一种是讲图片中的特征值进行提取生成向量,实现以图搜图模式的搜索。另一种是基于图片tag的方式,将tag进行向量化,这样可以搜索到语义相近的tag的图片,而不必完全相等。这两种方式在ES的词向量搜索中都可以支持。
  4. 社交网络:社交网络中的人都是一个单词,而其关注和粉丝都是和其相关的单词,因此可以每一个人的关注和粉丝形成一段“文本”去训练模型。想计算两个人是否相似或者两个的距离,只需要计算两个人的向量即可。

Elasticsearch的词向量搜索可以理解为提供了一个计算平台,而具体的应用场景需要自己评估是否适合。具体的效果好坏,其实还是取决于本身的模型训练质量和模型使用方式。

最佳实践

本例子以医疗领域的“智能问诊”为例进行了一个展示。在此说明这里仅仅是一个demo,重点介绍具体场景里如何使用Elasticsearch的向量搜索,其模型是否有更适合的或者效果是否满足用户使用在不做过多讨论。

预期功能

用户A生病了,在demo中输入一段症状描述,demo返回给用户得了什么病。

数据准备

demo需要准备的数据主要有两个:

  • 用以训练模型的文本数据(下方流程图的Texts):这是大量的和医疗相关的文本,可以是从维基百科爬取的整篇整篇的文章,或者免费版权的医学杂志、网站等获得的文本段落。该数据只要和医疗相关即可,格式为一行一个段落,如下:
xxxxx一行医学相关的文本,百姓所说的感冒是指“普通感冒”,又称“伤风”、急性鼻炎或上呼吸道感染。感冒是一种常见的急性上呼吸道病毒性感染性疾病,多由鼻病毒、副流感病毒、呼吸道合胞病毒、埃可病毒、柯萨奇病毒、冠状病毒、腺病毒等引起。临床表现为鼻塞、喷嚏、流涕、发热、咳嗽、头痛等,多呈自限性。大多散发,冬春季节多发,但不会出现大流行。 
一行医学相关的文本xxxxx 
一行医学相关的文本xxxxx
  • 专业的疾病描述文本数据(下方流程图的Data):比如“感冒:伴随有发烧、流鼻涕、浑身无尽...”,该数据用以和用户的输入进行匹配,返回给用户最相关的疾病。数据格式为json,其最重要的为具体的症状描述一栏,如下:
[
    {
        "id": "1",
        "name": "肝功能异常",
        "department": "消化科",
        "feature": "消化功能xxxxxxxxxxxxxxxxxxxxxxx\n"
    },
    {
        "id": "2",
        "name": "反胃",
        "department": "消化科",
        "feature": "xxxxxxxxxxxxxx为主要表现。\n"
    }
]
  • 停用词表:分词时候去除停用词的。数据格式为一行一行的单个单词,如下:
两者 
个
个别
临
为
为了
为什么

由于数据涉及到隐私,这里不进行提供,仅仅在源码中提供了数据的格式,方便跑通程序。

流程及代码实现

1:离线模型训练

这里将收集到的数据进行离线的顺联,生成 Doc2Vec 模型。离线训练模型特别花费时间,特别是在没有GPU的服务器上。该模型离线训练好后,后续会一直使用。

# 停用词
stopwords = [line.strip() for line in open('./data/ChineseStopWords.txt', encoding='UTF-8').readlines()]


def segment(sentence: str):
    """
    结巴分词,并去除停用词
    """
    resp = []
    sentence_depart = jieba.cut(sentence.strip())
    for word in sentence_depart:
        if word not in stopwords:
            if word != "":
                resp.append(word)
    return resp


def read_corpus(f_name):
    """
    读数据
    """
    with open(f_name, encoding="utf-8") as f:
        for i, line in enumerate(f):
            yield gensim.models.doc2vec.TaggedDocument(segment(line), [i])


def train():
    """
    训练 Doc2Vec 模型
    """
    train_file = "./data/train_data.txt"
    train_corpus = list(read_corpus(train_file))
    model = gensim.models.doc2vec.Doc2Vec(vector_size=300, min_count=2, epochs=10)
    print(len(train_corpus))
    model.build_vocab(train_corpus)
    model.train(train_corpus, total_examples=model.corpus_count, epochs=model.epochs)
    model.save("doc2vec.model")

2~3:特征数据转化为向量,并存到ES中

从数据库中将我们标注好的疾病描述的数据拿出来,利用之前训练的模型,将每一个疾病的描述转化为向量,然后存在ES中。该向量具有表达一个疾病的含义,其是对疾病描述的embedding,在后续匹配过程中,只需要将用户输入的向量和ES中的向量进行匹配,即可找到最相关的向量。

因此,这一步,也是一个离线的过程,其包括:

  1. ES中使用指定的mapping创建索引。这里需要将向量这个Field“feature_vector”的类型设置为“dense_vector”,由于我们在model训练期间设置的纬度是300,这里需要指定dims为300.
def create_index():
    print("begin create index")
    setting = {
        "settings": {
            "number_of_replicas": 0,
            "number_of_shards": 2
        },
        "mappings": {
            "properties": {
                "name": {
                    "type": "keyword"
                },
                "department": {
                    "type": "keyword"
                },
                "feature": {
                    "type": "text"
                },
                "feature_vector": {
                    "type": "dense_vector",
                    "dims": 300
                }
            }
        }
    }
    get_es_client().indices.create(index=indexName, body=setting)
    print("end create index")

2. 将文本数据转化为向量

def embed_text(sentences):
    """
    将所有的句子转化为向量
    """
    model = doc2vec.Doc2Vec.load("doc2vec.model")
    resp = []
    for s in sentences:
        resp.append(model.infer_vector(segment(s)).tolist())
    return resp

3. 将元数据和向量一起索引到es中

def bulk_index_data():
    """
    将数据索引到es中,且其中包含描述的特征向量字段
    """
    print("begin embed index data to vector")
    with open("./data/data.json") as file:
        load_dict = json.load(file)
    features = [doc["feature"] for doc in load_dict]
    print("number of lines to embed:", len(features))
    features_vectors = embed_text(features)
    print("begin index data to es")
    requests = []
    for i, doc in enumerate(load_dict):
        request = {'_op_type': 'index',  # 操作 index update create delete  
                   '_index': indexName,  # index
                   '_id': doc["id"],
                   '_source':
                       {
                           'name': doc["name"],
                           'department': doc["department"],
                           'feature': doc["feature"],
                           'feature_vector': features_vectors[i],
                       }
                   }
        requests.append(request)
    bulk(get_es_client(), requests)
    print("end index data to es")

4~8:用户输入症状表现,并转化为向量,从ES中搜索最相关的TopN个疾病

用户输入,我们假设从命令行输入即可。转化为向量也是使用最初训练的model进行了embed text,函数为上一个步骤使用过的embed_text。当用户的症状描述转化为一个向量时候,这时候即可从Es中进行搜索即可,在搜索的时候,需要使用Es的script_score的query,在query的scrip脚本中,将用户的向量放到查询语句的参数中,即可进行搜索,这里的搜索不是简单的文本匹配了,而是进行了语义层面的搜索。搜索结果中,我们将用户最大可能患有的疾病进行输出即可。

def test():
    model = doc2vec.Doc2Vec.load("doc2vec.model")
    es = get_es_client()
    while True:
        try:
            query = input("Enter query: ")
            input_vector = model.infer_vector(segment(query)).tolist()
            resp = es.search(index=indexName, body={
                "_source": ["name", "feature"],
                "query": {
                    "script_score": {
                        "query": {
                            "match_all": {}
                        },
                        "script": {
                            "source": "cosineSimilarity(params.queryVector, doc['feature_vector'])+1",
                            "params": {
                                "queryVector": input_vector
                            }
                        }
                    }
                }
            })
            print("可能获得的疾病是:", end=" ")
            for hit in resp["hits"]["hits"]:
                print(hit["_source"]["name"], end="\t")
            print("\n")
        except KeyboardInterrupt:
            return

效果

Enter query: 我眼睛充血,怎么办?
可能获得的疾病是: 红眼病 眼角膜发炎 外伤


Enter query: 呼吸不畅,咳嗽,胸闷是怎么回事?
可能获得的疾病是: 肺炎  上呼吸道感染   支气管炎

摘自: Elasticsearch: 基于Text Embedding的文本相似性搜索 - 知乎

相关文档:

https://www.woshipm.com/pmd/5541932.html
https://www.woshipm.com/u/1142016/page/3
https://www.woshipm.com/pmd/5541949.html
https://www.woshipm.com/it/5545840.html
https://www.woshipm.com/it/5545840.html#toc-3
https://www.woshipm.com/pmd/5572342.html
https://www.6aiq.com/article/1601296161290
https://developer.aliyun.com/article/1207600
https://zhuanlan.zhihu.com/p/107663526
https://zhuanlan.zhihu.com/p/80737146

<think>嗯,我现在要了解ElasticSearch向量搜索原理。之前对Elasticsearch有一些基础了解,知道它是一个基于Lucene的分布式搜索引擎,常用于全文检索、日志分析等场景。但向量搜索这部分还不太清楚,需要仔细研究一下。 首先,向量搜索应该和机器学习中的向量嵌入有关吧。比如,把文本、图片转换成高维向量,然后通过比较向量间的相似度来找到相似的内容。ElasticSearch作为搜索引擎,如何支持这种向量搜索呢?可能是在索引阶段存储这些向量,然后在查询时进行相似度计算。 接下来,可能需要了解ElasticSearch如何处理向量。听说它支持dense_vector数据类型,用来存储稠密向量。那在创建索引的时候,需要定义一个字段为dense_vector,指定维度。然后,插入文档时,该字段存储对应的向量值。 查询的时候,应该使用某种相似度度量方法,比如余弦相似度、欧氏距离或者点积。ElasticSearch可能提供了相关的查询方式,比如使用script_score查询,在查询时计算向量之间的距离,并按照相似度排序。 不过,这里有个疑问,向量搜索在大数据量下如何保证效率?传统的相似度计算是O(n)的复杂度,当数据量很大时,逐个计算会很慢。ElasticSearch是否采用了近似最近邻(ANN)算法,比如使用局部敏感哈希(LSH)、分层导航小世界(HNSW)或者树结构来加速搜索?可能需要查证一下。 另外,ElasticSearch的官方文档可能有相关的说明。比如,在7.0版本之后开始支持向量搜索,使用dense_vector字段类型,并且在进行相似度计算时,可以使用cosineSimilarity、l1norm、l2norm等函数。同时,可能结合了HNSW这样的近似算法来构建索引,提高搜索速度。 还有,关于查询的具体步骤,比如如何构建查询请求,是否需要使用特定的脚本或者查询DSL。可能需要编写一个使用script_score的例子,在查询时引用存储的向量字段,并指定相似度计算函数,然后进行排序。 不过,可能ElasticSearch向量搜索功能还在不断演进,不同版本之间可能有差异。比如,是否支持在索引时构建ANN结构,或者仅在查询时进行暴力计算?这会影响性能和适用场景。 总结一下,可能的流程是: 1. 数据预处理:将文本、图像等转换为向量。 2. 索引阶段:在ElasticSearch中定义dense_vector字段,存储向量。 3. 查询阶段:将查询内容转换为向量,使用脚本查询计算与索引中向量的相似度,并按评分排序。 4. 优化:可能使用ANN技术提高搜索效率,减少计算量。 不过需要确认ElasticSearch具体实现的技术细节,比如是否内置了ANN索引结构,还是依赖其他方式优化。例如,有些资料提到ElasticSearch使用了HNSW算法来构建向量索引,从而支持高效的近似最近邻搜索,但这可能需要确认版本支持情况。 另外,关于相似度计算的具体函数和参数设置,比如余弦相似度是否需要进行向量归一化处理,或者是否在计算时自动处理。这些细节可能影响最终搜索结果的相关性。 最后,可能需要考虑实际应用中的挑战,比如向量维度高带来的存储和计算开销,如何平衡准确性和速度,以及与其他向量数据库(如FAISS、Milvus)相比的优缺点。</think>Elasticsearch向量搜索原理可分为以下步骤,结合索引结构、相似度计算和优化策略实现高效检索: --- ### **1. 向量存储:dense_vector数据类型** - **定义字段**:创建索引时,需声明`dense_vector`类型字段,并指定维度(如128维)。 ```json PUT my_index { "mappings": { "properties": { "my_vector": { "type": "dense_vector", "dims": 128 } } } } ``` - **写入数据**:插入文档时,直接存储向量值。 ```json POST my_index/_doc { "my_vector": [0.12, -0.45, ..., 1.23] // 128维浮点数组 } ``` --- ### **2. 相似度计算** Elasticsearch通过脚本查询计算向量间的距离,常用方法包括: - **余弦相似度**(需手动归一化向量): ```json "script_score": { "script": { "source": "cosineSimilarity(params.query_vector, 'my_vector') + 1.0", "params": {"query_vector": [0.1, -0.2, ...]} } } ``` - **欧氏距离(L2范数)**: ```json "source": "1 / (1 + l2norm(params.query_vector, 'my_vector'))" ``` - **点积**(适合已归一化的向量): ```json "source": "dotProduct(params.query_vector, 'my_vector')" ``` --- ### **3. 近似最近邻(ANN)优化** 为提升大规模数据下的性能,Elasticsearch **8.0+** 引入**HNSW算法**: - **索引配置**:通过`index_options`启用HNSW。 ```json "my_vector": { "type": "dense_vector", "dims": 128, "index": true, "index_options": { "type": "hnsw", "m": 16, // 节点邻居数 "ef_construction": 100 // 构建时的搜索范围 } } ``` - **查询优化**:搜索时指定`knn`查询,利用HNSW图快速定位近似结果。 ```json { "knn": { "field": "my_vector", "query_vector": [0.1, -0.2, ...], "k": 10, "num_candidates": 100 } } ``` --- ### **4. 整体流程** 1. **数据准备**:通过模型(如BERT、ResNet)将文本/图像转换为向量。 2. **索引构建**:存储向量并建立HNSW图结构(若启用)。 3. **查询阶段**: - 将查询内容转换为向量。 - 遍历HNSW图快速缩小候选集(避免全量计算)。 - 精确计算Top候选的相似度得分,返回最终结果。 --- ### **5. 性能权衡** - **精度 vs 速度**:HNSW通过调节`ef_search`(搜索时的候选数)平衡两者。增大`ef_search`提升召回率,但增加延迟。 - **资源消耗**:向量维度越高,内存和CPU开销越大。通常需限制维度(如512以内)。 --- ### **6. 应用场景** - **语义搜索**:匹配相似含义的文本(如“手机”匹配“智能手机”)。 - **图像/视频检索**:以图搜图、内容推荐。 - **多模态搜索**:联合文本、图像向量进行跨模态检索。 --- ### **对比专用向量数据库** - **优势**:无缝集成Elasticsearch的全文检索、过滤、聚合能力。 - **局限**:ANN性能弱于FAISS、Milvus等专用库,适合中等规模数据(千万级)。 建议结合业务规模和数据特性选择方案,若需复杂过滤+向量搜索,Elasticsearch仍是高效选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值