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
相关推荐
Data_agent10 分钟前
1688获得1688店铺列表API,python请求示例
开发语言·python·算法
2401_8712600212 分钟前
Java学习笔记(二)面向对象
java·python·学习
2301_7644413341 分钟前
使用python构建的应急物资代储博弈模型
开发语言·python·算法
喏喏心1 小时前
深度强化学习:价值迭代与Bellman方程实践
人工智能·python·学习·机器学习
小白勇闯网安圈1 小时前
supersqli、web2、fileclude、Web_python_template_injection
python·网络安全·web
天远数科1 小时前
前端全栈进阶:使用 Node.js Crypto 模块处理 AES 加密与天远API数据聚合
大数据·api
天远API1 小时前
后端进阶:使用 Go 处理天远API的 KV 数组结构与并发风控
大数据·api
用户8356290780511 小时前
从一维到二维:用Spire.XLS轻松将Python列表导出到Excel
后端·python
千匠网络1 小时前
S2B供应链平台:优化资源配置,推动产业升级
大数据·人工智能·产品运营·供应链·s2b
WX-bisheyuange2 小时前
基于Spring Boot的智慧校园管理系统设计与实现
java·大数据·数据库·毕业设计