ES作为向量库研究

正文

相比专门的向量库,ES作为向量库有它自己的优点,比如可以方便的使用全文检索及混合检索。当然,性能和数据规模方面可能有所不如,不过,对于中等规模的应用,倒不是太大的问题。

向量索引创建与查询

比如我要建一个名为knowledge的向量索引,相关创建与查询DSL如下(kibana dev-tool工作台里输入即可):

PUT knowledge
{
  "mappings": {
    "properties": {
      "content": { "type": "text","analyzer": "standard" },
      "content_vector": {
        "type": "dense_vector",
        "dims": 1024,
        "index": true,
        "similarity": "cosine" // 距离函数:l2_norm / cosine / dot_product
      }
    }
  }
}

// 查看索引信息
GET /knowledge/_mapping

// 全量搜索
GET /knowledge/_search
{
  "size": 10,
  "fields": ["content"],
  "query": { "match_all": {} }
}

// 文本搜索
GET /knowledge/_search
{
  "query": {
    "match": {
      "content": "accept"
    }
  }
}

// 向量搜索
GET /knowledge/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [ -2.12,0.06,1.07,-0.90,......2.40,0.30,0.51 ], //这里省略了长向量内容
    "k":5,
    "num_candidates": 100
  }
}

这里有个小疑问:我们能用knowledge索引存储不同嵌入模型生成的向量吗?比如有的模型向量维度是768、有的是384。

答案是否定的。向量长度dims必须等于嵌入模型生成的向量长度。

ES作为RAG的向量库

保存向量到ES

使用前文的knowledge索引存储向量,代码如下:

		es = Elasticsearch("http://localhost:9200")
        vectorstore = ElasticsearchStore(
            es_connection=es,
            index_name=VEC_INDEX,
            embedding=embeddings,  # 关键:指定本地模型,本例中是bge-m3
            strategy="DenseVectorStrategy",  # 纯稠密向量
            ## 必须明确指定文本和对应的向量字段,否则默认使用text作为文本字段名,vector作为向量字段名
            query_field=TXT_FLD,  # 文本字段
            vector_query_field=VEC_FLD,  # 向量字段
        )

        # 4. 写入
        vectorstore.add_documents(splits)

    	LOGGER.info("persist to vectordb success")

用ES做向量检索

ES向量索引建好后,使用langchain就可以很方便的挂接到LLM上了,代码如下:

		vectorstore = ElasticsearchStore(
            es_connection=Elasticsearch("http://localhost:9200"),
            index_name=VEC_INDEX,
            embedding=embeddings,  # 关键:指定本地模型,本例中是bge-m3
            query_field=TXT_FLD,  # 文本字段
            vector_query_field=VEC_FLD,  # 向量字段
        )
        
        # --------- 检索器 ---------
        retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
        ......
        
        # --------- LLM(Ollama 本地 CPU 可跑) ---------
    llm = Ollama(model="qwen2.5:1.5b")
        
        # --------- 构建 RAG 链 ---------
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",  # stuff简单合并上下文
        retriever=retriever,
        return_source_documents=True)

ElasticsearchStore是langchain对ES作为向量库的封装,与其它类型的向量库(如chroma等)统一接口。

langchain内部使用前文提到的ES knn查询先得到一些知识,再将这些知识简单合并,组装为prompt提供给LLM,让LLM自己组织语言回答问题,这就是RAG。

用ES做混合检索

所谓混合检索,就是“文本检索+向量检索”同时进行的检索方式,当然,只是简单的合并两种检索的结果,效果并不好,因为两种方式的打分维度都不一样。一般说来,要利用所谓RRF(倒数排名融合)算法对两种检索的结果进行融合。

但这个RRF在ES8.11里是收费特性,直接使用会报错:

current license is non-compliant for [Reciprocal Rank Fusion (RRF)]

网上说升级到ES8.16+版本,用retriever机制可以解决:

"retriever": {
        "rrf": {
            "retrievers": [
                {
                    "standard": {
                        "query": {
                            "match": {
                                "content": {
                                    "query": q
                                }
                            }
                        }
                    }
                },
                {
                    "knn": {
                        "field": "content_vector",
                        "query_vector": vec,
                        "k": 3,
                        "num_candidates": 100
                    }
                }
            ],
            "rank_window_size": 100,
            "rank_constant": 60
        }
    }

但ES升级并实测下来,依然报相同的错误。估计,RRF确实就是个收费特性

当然,rrf算法本身不复杂,实现上,完全可以查两次ES,在内存里重排,这是示例代码:

from typing import List, Dict

def rrf(text_hits: List[Dict], vector_hits: List[Dict],
        k: int = 60, top_n: int = 10) -> List[str]:
    scores = {}                       # doc_id -> RRF 分数
    for rank, hit in enumerate(text_hits, start=1):
        scores[hit['_id']] = scores.get(hit['_id'], 0) + 1.0 / (k + rank)
    for rank, hit in enumerate(vector_hits, start=1):
        scores[hit['_id']] = scores.get(hit['_id'], 0) + 1.0 / (k + rank)

    # 按分数倒序,取 top_n
    return sorted(scores, key=lambda x: scores[x], reverse=true)[:top_n]

也可使用python的ranx三方库,该库提供了排名评估与融合的算法。

附录

正文的例子都是基于本地搭建的ES环境。

docker搭建ES+Kibana环境

参考网上的例子配置国内镜像仓:

{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.1panel.live",
    "https://docker.m.daocloud.io",
    "https://hub.rat.dev",
    "https://docker.1panel.top",
    "https://docker.ketches.cn",
    "https://docker.chenby.cn",
    "https://dockerpull.org",
    "https://dockerhub.icu",
    "https://docker.unsee.tech",
    "https://mirrors.ustc.edu.cn",
    "https://mirror.azure.cn"
  ]
}

接着拉取镜像,例如:

docker pull docker.1ms.run/elasticsearch:8.11.1
docker pull docker.1ms.run/kibana:8.11.1

最后,两个镜像用docker-compose组合运行(需先用apt install提前安装docker-compose):

# docker-compose.yml
version: "2.4"
services:
  es:
    image: docker.1ms.run/elasticsearch:8.11.1
    container_name: es8
    environment:
      - discovery.type=single-node            # 单节点
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
      - xpack.security.enabled=false          # 测试环境关闭 TLS/密码
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - elk

  kibana:
    image: docker.1ms.run/kibana:8.11.1
    container_name: kib8
    depends_on:
      - es
    environment:
      - ELASTICSEARCH_HOSTS=http://es:9200
    ports:
      - "5601:5601"
    networks:
      - elk

volumes:
  esdata:

networks:
  elk:

执行命令:

docker-compose up -d

-d表示后台执行,不加的话,信息都打在控制台上了。

通过下面网址登kibana即可:

http://localhost:5601/app/home#/

停掉服务用:

docker-compose stop

停掉服务,还要删掉容器、网络和卷,用:

docker-compose down

有时候,我们要使用同一组件的新版本,就需要用down命令把老的容器、网络和卷都删掉,仅用stop是不行的。

查看所有服务状态,用:

docker-compose ps
<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仍是高效选择。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值