ES作为向量库研究

正文

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

向量索引创建与查询

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

json 复制代码
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索引存储向量,代码如下:

python 复制代码
		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上了,代码如下:

python 复制代码
		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机制可以解决:

json 复制代码
"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,在内存里重排,这是示例代码:

python 复制代码
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环境

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

json 复制代码
{
  "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):

yaml 复制代码
# 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
相关推荐
龙信科技3 分钟前
【国内电子数据取证厂商龙信科技】Windows Prefetch预读取文件取证分析
大数据·科技·电子数据取证
MC皮蛋侠客11 分钟前
TDengine C++ 系列(1):全景与最小闭环——从时序数据到第一个 C++ 读写
大数据·c++·tdengine
AC赳赳老秦16 分钟前
网页公开附件自动采集:OpenClaw 批量下载页面内嵌 Word/Excel 附件,统一格式后结构化入库
java·python·word·php·excel·deepseek·openclaw
北斗落凡尘16 分钟前
LangGraph 入门实战(3)
python·langchain
TAN-90°-27 分钟前
Deep Learning for Computer Vision——Image Classification with Linear Classifiers
python·深度学习·算法·计算机视觉·线性回归
AINative软件工程29 分钟前
LLM API 成本失控怎么办?工程师的实时异常检测指南
python
练习两年半的攻城狮29 分钟前
LlamaIndex ResponseMode 深度解析
python·llamaindex
生活皆是风景34 分钟前
抢占AI采信,问答布局正当时
大数据·安全·搜索引擎
生态学者41 分钟前
Journal of Applied Ecology | 华南植物园王法明研究员团队揭示加纳红树林蓝碳储量及其环境调控机制
大数据·r语言·微信公众平台
阿pin1 小时前
Java随笔-红黑树
java·python·算法·红黑树