Metadata-Version: 2.4
Name: openai-simple-vectorstore
Version: 0.3.1
Summary: 可扩展的多向量数据库接入库（redis-search / milvus / milvus-lite / pgvector / elasticsearch / sqlite-vec / qdrant），集成 embeddings 与 rerank 模型，支持二阶段召回及插入、删除、刷新等管理功能。
Author: rRR0VrFP
Maintainer: rRR0VrFP
License-Expression: MIT
Project-URL: Homepage, https://gitee.com/rRR0VrFP/openai-simple-vectorstore
Keywords: openai-simple-vectorstore,vectorstore,redis-search,milvus,milvus-lite,pgvector,elasticsearch,sqlite-vec,qdrant,embeddings,rerank
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML
Requires-Dist: httpx
Requires-Dist: numpy
Requires-Dist: pydantic>=2
Requires-Dist: python-environment-settings
Requires-Dist: zenutils
Provides-Extra: redis
Requires-Dist: redis; extra == "redis"
Requires-Dist: redisvl>=0.27.1; extra == "redis"
Provides-Extra: milvus
Requires-Dist: pymilvus>=2.4; extra == "milvus"
Provides-Extra: milvus-lite
Requires-Dist: pymilvus>=2.4; extra == "milvus-lite"
Requires-Dist: milvus-lite; extra == "milvus-lite"
Provides-Extra: pgvector
Requires-Dist: psycopg2-binary; extra == "pgvector"
Provides-Extra: elasticsearch
Requires-Dist: elasticsearch>=8.0; extra == "elasticsearch"
Provides-Extra: sqlite-vec
Requires-Dist: sqlite-vec; extra == "sqlite-vec"
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.7; extra == "qdrant"
Provides-Extra: all
Requires-Dist: redis; extra == "all"
Requires-Dist: redisvl>=0.27.1; extra == "all"
Requires-Dist: pymilvus>=2.4; extra == "all"
Requires-Dist: milvus-lite; extra == "all"
Requires-Dist: psycopg2-binary; extra == "all"
Requires-Dist: elasticsearch>=8.0; extra == "all"
Requires-Dist: sqlite-vec; extra == "all"
Requires-Dist: qdrant-client>=1.7; extra == "all"
Dynamic: license-file

# openai-simple-vectorstore

可扩展的多向量数据库接入库，内置 **redis-search**、**milvus**、**milvus-lite**、
**pgvector**、**elasticsearch**、**sqlite-vec** 与 **qdrant** 等后端，并提供统一的 embeddings / rerank
二阶段召回、插入、删除、刷新等管理接口。

- redis-search 后端与 [openai-redis-vectorstore](https://gitee.com/rRR0VrFP/openai-redis-vectorstore)
  行为兼容（相同的 uid 规则与 relevance score，生成的 key 布局一致），但索引 `PREFIX` 由
  上游的 `index_name` 改为 `index_name + ":"`，以隔离同名前缀的其它索引（见版本记录）。
- 通过工厂 + 注册表机制可低成本扩展其它向量数据库后端。

## 安装

作为业务方使用时，从 PyPI 安装本库并选择所需后端：

```bash
# 仅核心依赖（不含任何后端驱动，需配合下面的某个后端 extra 使用）
pip3 install openai-simple-vectorstore

# redis-search 后端（也是 create_vector_store() 的默认后端）
pip3 install "openai-simple-vectorstore[redis]"

# 仅 milvus 后端（依赖 pymilvus）
pip3 install "openai-simple-vectorstore[milvus]"

# 仅 milvus-lite 后端（嵌入式 milvus，无需单独部署，适合本地开发/测试）
pip3 install "openai-simple-vectorstore[milvus-lite]"

# 仅 pgvector 后端（依赖 psycopg2-binary）
pip3 install "openai-simple-vectorstore[pgvector]"

# 仅 elasticsearch 后端
pip3 install "openai-simple-vectorstore[elasticsearch]"

# 仅 sqlite-vec 后端（嵌入式 SQLite，无需单独部署）
pip3 install "openai-simple-vectorstore[sqlite-vec]"

# 仅 qdrant 后端（依赖 qdrant-client）
pip3 install "openai-simple-vectorstore[qdrant]"

# 全部后端
pip3 install "openai-simple-vectorstore[all]"
```

> 在仓库源码目录内本地开发时使用 `pip3 install -e ".[all]"`；离线安装本库自身依赖时，
> 先准备本地 wheel 目录，再执行
> `pip3 install --no-index --find-links <wheel 目录> -r requirements.txt`。

## 快速开始

### 选择 redis-search 后端（默认）

> 需先安装 redis 后端：`pip3 install "openai-simple-vectorstore[redis]"`。

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store()  # 默认后端由 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB 决定

# 插入
uid = vs.insert("今天天气很好", kb_id="kb1", doc_id="doc1", page_id="p1")

# 二阶段召回（向量检索 + rerank 重排）
docs = vs.similarity_search_and_rerank(
    query="今天天气怎么样",
    index_name="default",
    k=3,
)
for doc in docs:
    print(doc.vs_page_content, doc.vs_embeddings_score, doc.vs_rerank_score)

# 删除 / 刷新
vs.delete(uid)
vs.flush("default")
```

### 选择 milvus 后端

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=milvus
    vector_db="milvus"
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```

### 选择 milvus-lite 后端（嵌入式，本地开发/测试）

milvus-lite 与 milvus 使用相同的 `MilvusClient` API，仅连接方式不同：milvus 通过
`http://host:19530` 连接服务端，milvus-lite 通过本地文件路径启动进程内嵌入式数据库，
无需部署 milvus 服务。

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(
    vector_db="milvus-lite",  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=milvus-lite
    milvus_lite_uri="./milvus_lite.db",  # 默认 OPENAI_SIMPLE_VECTORSTORE_MILVUS_LITE_URI
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```


### 选择 sqlite-vec 后端（嵌入式，本地开发/测试）

sqlite-vec 是 SQLite 的向量搜索扩展，所有数据存在本地文件中，无需单独部署服务。
注意：需要 sqlite3 支持 loadable-extension（多数发行版 CPython 均支持）。

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(
    vector_db="sqlite-vec",  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=sqlite-vec
    sqlite_vec_uri="./sqlite_vec.db",  # 默认 OPENAI_SIMPLE_VECTORSTORE_SQLITE_VEC_URI
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```

### 选择 pgvector 后端（PostgreSQL 扩展）

需要 PostgreSQL 已安装 pgvector 扩展（程序启动时会尝试 `CREATE EXTENSION IF NOT EXISTS vector`）：

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(
    vector_db="pgvector",  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=pgvector
    pgvector_url="postgresql://user:pass@localhost:5432/postgres",  # 默认 OPENAI_SIMPLE_VECTORSTORE_PGVECTOR_URL
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```

### 选择 elasticsearch 后端

需要 Elasticsearch 8.x（dense_vector + HNSW）：

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(
    vector_db="elasticsearch",  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=elasticsearch
    es_url="http://localhost:9200",
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```

### 选择 qdrant 后端

需要部署 Qdrant 服务（或使用 Qdrant Cloud）。使用原生 payload 过滤，无需像
milvus / sqlite-vec 那样对候选集做放大：

```python
from openai_simple_vectorstore import create_vector_store

vs = create_vector_store(
    vector_db="qdrant",  # 或环境变量 OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB=qdrant
    qdrant_url="http://localhost:6333",  # 默认 OPENAI_SIMPLE_VECTORSTORE_QDRANT_URL
    qdrant_api_key=None,                 # 默认 OPENAI_SIMPLE_VECTORSTORE_QDRANT_API_KEY
    # qdrant_prefer_grpc=True,            # 默认 OPENAI_SIMPLE_VECTORSTORE_QDRANT_PREFER_GRPC
    # qdrant_timeout=60,                  # 客户端请求超时（秒），默认 OPENAI_SIMPLE_VECTORSTORE_QDRANT_TIMEOUT
    # qdrant_on_disk=False,               # 总开关，默认 OPENAI_SIMPLE_VECTORSTORE_QDRANT_ON_DISK（False）
    # qdrant_vector_on_disk=True,         # 向量是否落盘，覆盖总开关
    # qdrant_hnsw_on_disk=True,           # HNSW 图是否落盘，覆盖总开关
    # qdrant_payload_on_disk=True,        # payload 索引是否落盘，覆盖总开关
)
vs.insert("..." , kb_id="kb1", doc_id="doc1", page_id="p1")
docs = vs.similarity_search_and_rerank(query="...", index_name="default", k=3)
```

qdrant 的向量、HNSW 图、payload 索引三部分是否落盘可**分别**配置：
`qdrant_vector_on_disk` / `qdrant_hnsw_on_disk` / `qdrant_payload_on_disk`
（对应环境变量 `OPENAI_SIMPLE_VECTORSTORE_QDRANT_{VECTOR,HNSW,PAYLOAD}_ON_DISK`）。
三者默认均为 `False`，即默认全部驻留内存，以获得最低检索延迟。`qdrant_on_disk`
是给三者兜底的总开关（默认 `False`）：未单独设置的项会继承它，单独设置项优先。
落盘（mmap）可避免大图常驻内存，但检索时的随机读会变慢，建议搭配 NVMe SSD。

本地快速启动（podman）：

```bash
podman run -d --name qdrant -p 6333:6333 -p 6334:6334 docker.io/qdrant/qdrant:latest
```

### 直接实例化（不依赖全局配置）

```python
from openai_simple_vectorstore import RedisVectorStore
from openai_simple_vectorstore.base import Connection
from openai_simple_vectorstore.utils import YamlSerializer

vs = RedisVectorStore(
    redis_stack_url="redis://localhost:6379/0",
    embeddings_llm=Connection(base_url="http://localhost/v1", api_key="sk-xxx"),
    rerank_llm=Connection(base_url="http://localhost/v1", api_key="sk-xxx"),
    embeddings_model="bge-m3",
    rerank_model="bge-reranker-v2-m3",
    metadata_serializer=YamlSerializer(),
)
```

- `metadata_serializer` 决定业务自定义元数据（`metadata` / `vs_metadata`）持久化时的序列化方式，
  默认 `YamlSerializer`；也可传入 `JsonSerializer`（均定义于 `openai_simple_vectorstore.utils`）。

## 使用说明

`VectorStore` 对业务屏蔽后端差异，统一提供写入 / 检索 / 读取 / 删除 / 索引管理接口。
下面所有示例中的 `vs` 均指已创建的 `VectorStore` 实例。

### 写入

```python
# 单条：返回 uid（格式 <index_name>:<page_id>）
uid = vs.insert(
    "今天天气很好",
    page_id="p1",          # 缺省自动生成 uuid4
    kb_id="kb1",           # 缺省 "default"
    doc_id="doc1",         # 缺省 "default"
    category="news",       # 缺省 "default"
    index_name="default",
    metadata={"author": "alice", "score": 9.5},  # 业务自定义元数据，随记录持久化
)

# 批量：返回 uid 列表；texts / metadatas / page_ids 长度必须一致
uids = vs.insert_many(
    ["文本一", "文本二"],
    metadatas=[{"author": "alice"}, {"author": "bob"}],
    page_ids=["p1", "p2"],
    kb_id="kb1",
    index_name="default",
)
```

> 同一 `page_id` 重复写入是**覆盖**（upsert 语义），不会产生重复记录；调用方传入的
> `metadata` / `metadatas` 不会被就地修改。

### 检索

```python
# 仅向量检索：相关度 = 1 - distance，返回按相关度降序的 [Document, ...]
docs = vs.similarity_search_with_relevance_scores(
    "今天天气怎么样",
    index_name="default",
    k=5,
    kb_id="kb1",                       # 快捷过滤，等价 filters=[Filter("vs_kb_id", ["kb1"])]
    categories=["news", "tech"],       # 多值：命中其一即可
    embeddings_score_threshold=0.3,    # 低于阈值的结果直接丢弃（None 表示不过滤）
)

# 二阶段召回：先向量检索取 k*scale 条候选，再经 rerank 重排取前 k
docs = vs.similarity_search_and_rerank(
    "今天天气怎么样",
    index_name="default",
    k=3,
    scale=3,                           # 候选数 = k * scale，默认 2
    kb_ids=["kb1", "kb2"],
    rerank_score_threshold=0.5,
)

# 多索引联合检索（跨索引去重、统一排序）
docs = vs.similarity_search_with_relevance_scores(
    "查询", index_names=["idx_a", "idx_b"], k=10,
)
```

### 读取 / 删除 / 清空

```python
item = vs.get_item(uid)      # 返回元数据 dict（含 embedding 字节）；不存在返回 {}
count = vs.delete(uid)       # 返回实际删除条数
count = vs.delete_many(uids) # 按 index_name 自动分组批量删除
vs.flush("default")          # 清空该索引数据（破坏性；是否删除底层索引/表随后端而异）
```

> `flush` 一律清空该索引下的数据（破坏性）。是否同时删除底层索引 / 表随后端而异：
> redis / milvus / milvus-lite / qdrant / sqlite-vec 会删除索引或表；elasticsearch
> 仅清空文档并保留 index 与 mapping；pgvector 删除共享表中该索引对应的行。

### 检索结果字段（`Document`）

`similarity_search_*` 返回 `Document`，可用 `document_schema=YourModel` 换成自定义模型：

| 字段 | 含义 |
| --- | --- |
| `vs_page_content` | 原始文本内容 |
| `vs_embedding` | 向量（float32 字节） |
| `vs_embeddings_score` | 向量相关度 `1 - distance`，取值 `[0, 1]` |
| `vs_rerank_score` | rerank 分数（仅 `similarity_search_and_rerank`） |
| `vs_uid` | `<index_name>:<page_id>` |
| `vs_index_name` | 命中的索引名 |
| `vs_kb_id` / `vs_doc_id` / `vs_page_id` / `vs_category` | 核心元数据 |
| `vs_metadata` | 业务自定义元数据 `dict`（如 `{"author": ..., "score": ...}`） |

### 过滤

```python
from openai_simple_vectorstore.base import Filter

# tag 精确过滤：values 内多值为 OR 命中
docs = vs.similarity_search_with_relevance_scores(
    "查询", index_name="default", k=5,
    filters=[Filter(field="author", values=["alice", "bob"])],
)

# 多个 Filter 之间为 AND；numeric 用 op="range" 传 [low, high]
docs = vs.similarity_search_and_rerank(
    "查询", k=5,
    filters=[
        Filter(field="author", values=["alice"]),
        Filter(field="score", values=[7, 10], op="range"),
    ],
)

# 也接受 dict（等价 tag 精确过滤，多键为 AND）；dict 形式不支持 range
docs = vs.similarity_search_with_relevance_scores(
    "查询", k=5, filters={"author": "alice", "lang": "zh"},
)
```

> 过滤字段必须先在实例上声明，未声明 / 非法取值（空值、`None`、`low > high`）会明确报错，
> 而不是静默忽略。

### 声明可过滤字段

业务自定义元数据默认会随记录持久化并在 `vs_metadata` 中返回；**要作为过滤条件，必须显式声明**：

```python
from openai_simple_vectorstore.base import IndexField

# 简单方式：一批 tag 字段
vs = create_vector_store(filterable_metadata_fields=["author", "lang"])

# 精细方式（与上面二选一；一旦提供 declared_fields，则忽略 filterable_metadata_fields）
vs = create_vector_store(
    declared_fields=[
        IndexField(name="author", type="tag"),        # 字符串等值
        IndexField(name="tags", type="tag_multi"),    # 多值集合命中
        IndexField(name="score", type="numeric"),     # 数值，支持 op="range"
        IndexField(name="body", type="text"),         # 全文
    ],
)
```

- 自定义核心字段名：构造时传 `kb_id_field_name` / `doc_id_field_name` /
  `page_id_field_name` / `category_field_name`，检索结果的 `Document` 标准字段会自动回填。
- 更多字段模型与各后端过滤能力见下方《字段模型与过滤语义》。

### 索引管理

```python
# 建索引（幂等）：fields 优先于实例声明；已存在时仅增量补字段，不重建
vs.create_index("default", fields=[IndexField(name="author", type="tag")])

# 查看归一化 schema 与是否存在
info = vs.describe_index("default")
print(info.exists, [f.name for f in info.fields])

# 漂移检测与重建（重建是破坏性的，只能显式调用）
diff = vs.compare_index_schema("default")
if diff.drifted or not diff.exists:
    vs.rebuild_index("default")               # force=True：无条件重建
# vs.rebuild_index("default", force=False)    # 仅在检测到漂移时重建

# 原生过滤索引的检查与补建（缺字段时 qdrant 仅变慢，milvus/pgvector/sqlite-vec 由元数据兜底；redis/elasticsearch 可能报错或失配）
if vs.check_index_fields("default"):
    vs.build_index_fields("default")
```

### 连接与模型参数

```python
from openai_simple_vectorstore.base import Connection

vs = create_vector_store(
    vector_db="redis",
    redis_stack_url="redis://localhost:6379/0",
    embeddings_llm=Connection(base_url="http://localhost/v1", api_key="sk-xxx"),
    rerank_llm=Connection(base_url="http://localhost/v1", api_key="sk-xxx"),
    embeddings_model="bge-m3",
    rerank_model="bge-reranker-v2-m3",
    embeddings_dims=1024,             # 嵌入维度，需与模型输出一致（默认 1024）
    embeddings_max_size=8192,         # 单段文本上限，超出截断（默认 1024）
    rerank_max_size=8192,             # rerank 单段文本上限，超出截断（默认 1024）
    embeddings_score_threshold=None,  # None 表示不过滤，见下
    rerank_score_threshold=None,      # None 表示不过滤，见下
    redis_timeout=60,
)
```

- 只传 `llm` 时同时用于 embeddings 与 rerank；分别配置请用 `embeddings_llm` / `rerank_llm`。
- 各远程后端的超时可用 `redis_timeout` / `milvus_timeout` / `pgvector_timeout` /
  `es_timeout` / `qdrant_timeout` 覆盖（默认 60 秒）。
- 两个阈值语义相同：`None`（默认）表示不过滤；给出数字时，低于该分数的结果被丢弃。
  区别只在作用对象：`embeddings_score_threshold` 作用于向量相关度 `1 - distance ∈ [0, 1]`
  （因此设 `0.0` 等同于不过滤），`rerank_score_threshold` 作用于 rerank 模型分数
  （取值范围由模型决定，如 bge-reranker 近似 `[0, 1]`）。

## 环境变量

| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `OPENAI_SIMPLE_VECTORSTORE_VECTOR_DB` | `redis` | 后端：`redis` / `redis-search` / `milvus` / `milvus-lite` / `pgvector` / `elasticsearch` / `sqlite-vec` / `qdrant` |
| `OPENAI_SIMPLE_VECTORSTORE_REDIS_STACK_URL` | `redis://localhost:6379/0` | redis-stack 地址（redis 后端） |
| `OPENAI_SIMPLE_VECTORSTORE_REDIS_TIMEOUT` | `60` | redis 连接/读写超时秒数（别名 `REDIS_TIMEOUT`） |
| `OPENAI_SIMPLE_VECTORSTORE_MILVUS_URI` | `http://localhost:19530` | milvus 地址 |
| `OPENAI_SIMPLE_VECTORSTORE_MILVUS_TOKEN` | 空 | milvus 鉴权 token |
| `OPENAI_SIMPLE_VECTORSTORE_MILVUS_CONNECTION_ALIAS` | `default` | milvus 连接别名 |
| `OPENAI_SIMPLE_VECTORSTORE_MILVUS_TIMEOUT` | `60` | milvus RPC 超时秒数（别名 `MILVUS_TIMEOUT`） |
| `OPENAI_SIMPLE_VECTORSTORE_MILVUS_LITE_URI` | `./milvus_lite.db` | milvus-lite 本地数据库文件路径 |
| `OPENAI_SIMPLE_VECTORSTORE_PGVECTOR_URL` | `postgresql://localhost:5432/postgres` | pgvector 连接串（pgvector 后端） |
| `OPENAI_SIMPLE_VECTORSTORE_PGVECTOR_TIMEOUT` | `60` | pgvector 连接超时秒数（别名 `PGVECTOR_TIMEOUT`） |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_URL` | `http://localhost:9200` | elasticsearch 地址 |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_API_KEY` | 空 | elasticsearch API key（优先于账号密码） |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_USERNAME` | 空 | elasticsearch 用户名 |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_PASSWORD` | 空 | elasticsearch 密码 |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_VERIFY_CERTS` | `True` | 是否校验证书（自签 HTTPS 集群可设 `false`） |
| `OPENAI_SIMPLE_VECTORSTORE_ELASTICSEARCH_TIMEOUT` | `60` | elasticsearch 请求超时秒数（别名 `ELASTICSEARCH_TIMEOUT` / `ES_TIMEOUT`） |
| `OPENAI_SIMPLE_VECTORSTORE_SQLITE_VEC_URI` | `./sqlite_vec.db` | sqlite-vec 本地数据库文件路径 |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_URL` | `http://localhost:6333` | qdrant 服务地址（别名 `QDRANT_URL`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_API_KEY` | 空 | qdrant API key（别名 `QDRANT_API_KEY`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_PREFER_GRPC` | `False` | 是否优先使用 gRPC（别名 `QDRANT_PREFER_GRPC`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_TIMEOUT` | `60` | qdrant-client 请求超时秒数（别名 `QDRANT_TIMEOUT`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_ON_DISK` | `False` | 向量/HNSW/payload 是否落盘的总开关（别名 `QDRANT_ON_DISK`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_VECTOR_ON_DISK` | 继承总开关 | 向量列是否落盘（别名 `QDRANT_VECTOR_ON_DISK`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_HNSW_ON_DISK` | 继承总开关 | HNSW 图是否落盘（别名 `QDRANT_HNSW_ON_DISK`） |
| `OPENAI_SIMPLE_VECTORSTORE_QDRANT_PAYLOAD_ON_DISK` | 继承总开关 | payload 索引是否落盘（别名 `QDRANT_PAYLOAD_ON_DISK`） |
| `OPENAI_BASE_URL` | `http://localhost/v1` | OpenAI 兼容服务基础地址（embeddings / rerank 共用） |
| `OPENAI_API_KEY` | 空 | OpenAI 兼容服务密钥（embeddings / rerank 共用） |
| `OPENAI_EMBEDDINGS_BASE_URL` | 同 `OPENAI_BASE_URL` | embeddings 服务地址（单独覆盖） |
| `OPENAI_EMBEDDINGS_API_KEY` | 同 `OPENAI_API_KEY` | embeddings 服务密钥（单独覆盖） |
| `OPENAI_EMBEDDINGS_MODEL` | `bge-m3` | embeddings 模型名 |
| `OPENAI_EMBEDDINGS_MAX_SIZE` | `1024` | embeddings 单段文本上限，超出截断 |
| `OPENAI_EMBEDDINGS_DIMS` | `1024` | 嵌入向量维度（需与模型输出一致） |
| `OPENAI_RERANK_BASE_URL` | 同 `OPENAI_BASE_URL` | rerank 服务地址（单独覆盖） |
| `OPENAI_RERANK_API_KEY` | 同 `OPENAI_API_KEY` | rerank 服务密钥（单独覆盖） |
| `OPENAI_RERANK_MODEL` | `bge-reranker-v2-m3` | rerank 模型名 |
| `OPENAI_RERANK_MAX_SIZE` | `1024` | rerank 单段文本上限，超出截断 |

> 兼容 `openai-redis-vectorstore`：`OPENAI_REDIS_VECTORSTORE_REDIS_STACK_URL`、
> `OPENAI_EMBEDDINGS_*`、`OPENAI_RERANK_*`、`OPENAI_BASE_URL`、`OPENAI_API_KEY` 等变量可直接使用。

## 扩展新的向量数据库后端

继承共享抽象基类 `VectorStore` 并实现以下方法，再注册到工厂即可：

1. 实现 `get_cached_vectorstore`（引擎的获取与缓存）
2. 实现 `_search_index`，返回 `[(page_id, distance, item), ...]`
3. 实现 `get_item` / `delete` / `delete_many` / `flush`

```python
from openai_simple_vectorstore.base import VectorStore, IndexField
from openai_simple_vectorstore.registry import register_vector_store, create_vector_store

class MyStore(VectorStore):
    def get_cached_vectorstore(self, **kwargs): ...
    def _search_index(self, engine, query_embedding, index_name,
                      filters, filter_expression, k): ...
    def get_item(self, uid): ...
    def delete(self, uid): ...
    def delete_many(self, uids): ...
    def flush(self, index_name=None): ...

    # 可选的统一字段模型 / 能力矩阵 / schema 查询契约
    def _capabilities(self): ...
    def _describe_index(self, index_name): ...
    def _ensure_index_created(self, index_name, fields): ...

register_vector_store("mydb", MyStore)
vs = create_vector_store(vector_db="mydb")
```

`base` 会统一处理 embeddings 生成、rerank、过滤、去重、排序、`relevance_score` 与
`Document` 组装，后端只需专注各自的检索实现。

### 字段模型与过滤语义

各引擎通过统一的字段声明 / 能力矩阵 / schema 查询接口，向业务暴露"哪些扩展字段可过滤、索引现状如何"：

- **`IndexField`**：声明字段类型，支持 `tag` / `tag_multi` / `numeric` / `text` / `vector`；
  向量字段必须给出 `dims`，并与引擎配置的 `embeddings_dims` 一致。
- **`capabilities()`**：返回能力矩阵（`Capabilities`）——`indexed_types` 表示后端建成
  原生可过滤列的类型（如 redis、elasticsearch），`meta_filter_types` 表示在元数据上做
  Python/表达式侧兜底过滤的类型（如 milvus / sqlite-vec / pgvector 的部分类型）。
- **`describe_index(index_name)`**：返回归一化的 `IndexSchemaInfo`——`exists=False` 表示
  索引尚未创建（此时 `fields` 返回声明/期望字段，方便业务预创建）。索引存在且后端有可读的
  逐字段线上 schema 时返回线上真实字段（redis / elasticsearch / milvus / milvus-lite）；
  qdrant / pgvector / sqlite-vec 无逐字段线上 schema（payload / metadata JSON），`fields`
  恒为声明字段。
- **`create_index(index_name, fields)`**：按声明的 `IndexField` 列表构建索引（显式传入的
  `fields` 优先于实例构造时的 `declared_fields`）；已存在时仅补齐可增量添加的部分
  （redis `FT.ALTER` / elasticsearch `put_mapping` / qdrant payload 索引），不会重建。
- **`compare_index_schema(index_name)` / `rebuild_index(index_name, force=...)`**：只读检测
  线上 schema 与期望是否漂移；返回的 `IndexDiff` 中 `exists=False` 表示索引已不存在
  （如被外部删除），`drifted=True` 表示字段 / 维度 / 距离度量不一致。重建一律是破坏性的
  （删除并重新索引、丢失该索引下的数据），只能显式调用 `rebuild_index`（`force=True`
  无条件重建，`force=False` 仅在检测到漂移时重建），不会在创建 / ensure 索引时自动触发。
- **`check_index_fields(index_name)` / `build_index_fields(index_name)`**：检查声明字段中
  哪些缺少后端原生过滤索引（`IndexFieldStatus.missing`），并按需补建。补建是显式 API，
  不会在 ensure / 写入路径上隐式执行——缺字段时 qdrant 仍能正确过滤（仅变慢），
  milvus / pgvector / sqlite-vec 由元数据侧兜底，redis / elasticsearch 则会过滤报错或
  失配；建议由调用方主动调用补建。例外：首次创建索引（redis `FT.CREATE` /
  elasticsearch 建 index / qdrant 建 collection）时会按当时声明一并建好原生索引，
  仅当之后新增字段时才需要显式补建。

过滤统一使用 `Filter` 对象（`tag` 精确、`tag_multi` 多值命中、`numeric` 区间 via `op="range"`）；
`base` 把归一化后的 `filters` 传入各后端的 `engine/search`。后端只需实现
`_build_filter_expression(filters, filter_expression)` 把归一化过滤器组装为原生过滤表达式。

- **索引初始化一次性**：引擎实例首次执行 `ensure_*`（建表 / 建 collection / 建 index）
  时完成创建或存在性探测，之后所有读写路径不再重复探测（内部 `_index_ready` /
  `_table_ready` 标记）。若索引被外部删除或 schema 漂移，用 `compare_index_schema`
  检测后显式 `rebuild_index`，而不是期望每次操作都自动 `ensure`。
- **redis `tag_multi` 的逗号限制**：RediSearch 对 HASH 字段的 TAG 以逗号分隔且
  **不解析反斜杠转义**（实测 `a\,b` 会被拆成两个标签），因此单个标签值内部不能包含
  逗号；普通多值标签（元素本身不含分隔符）可被正确索引与过滤。

## 概念说明

- **uid**：`<index_name>:<page_id>`，用于唯一定位一条记录。
- **relevance_score**：`1 - distance`，取值 `[0, 1]`，越大越相关。
- **二阶段召回**：`similarity_search_and_rerank` 先用向量检索取 `k * scale` 条候选，
  再经 rerank 重排取前 `k` 条。
- **检索错误诊断**：任一后端的向量检索（含超时、连接失败等）都会在抛出前
  记录并返回带**后端地址 + 索引名 + 检索参数**的 `RuntimeError`，便于定位，
  不会把底层驱动的原始调用堆栈直接抛给调用方；索引不存在（未初始化）仍按
  空结果处理。
- **连接/查询超时**：各远程后端默认 60 秒（`OPENAI_SIMPLE_VECTORSTORE_*_TIMEOUT`，
  qdrant 见上文），也可在实例化时用 `redis_timeout` / `milvus_timeout` /
  `pgvector_timeout` / `es_timeout` 覆盖（`qdrant_timeout` 同理）。
- **elasticsearch 批量写入非事务**：`insert_many` 逐条写入，任一条失败时会**尽力**
  回滚（删除）本次已写入的文档后再抛出，尽量表现为“全成功或全不写入”，但 ES 无多
  文档事务，回滚本身失败时仅记录 warning，不掩盖原始异常。
- **elasticsearch numeric 字段**：`numeric` 映射为 `float`，ES 默认
  `coerce=true`，因此字符串数值（如 `"7"`）会被接受并转换为数值，属预期行为。

## 开发与测试

```bash
python3 -m pytest -q
```

跑 `pytest` 时的覆盖率门槛在 `pytest.ini` 中（`--cov-fail-under=90`）；
`.coveragerc` 的 `fail_under=80` 只作用于单独执行 `coverage report` 的场合。

测试分两类：

- **单元测试**（`test_unit.py` / `test_engines_*.py`）：全部基于内存 fake，离线可跑。
- **真实嵌入式/外部服务集成测试**（`test_engines_integration.py`）：优先使用真实数据库
  跑完整链路（本地 OpenAI 兼容 HTTP stub 提供 embeddings/rerank，覆盖 base 真实 HTTP 路径）：
  - `milvus-lite`：进程内嵌入，直接运行；
  - `sqlite-vec`：需要 sqlite3 支持 loadable-extension，否则自动跳过
    （本机 python.org 构建默认不支持，可用支持扩展的 Homebrew python3 执行）；
  - `pgvector`：外部服务，通过 `OPENAI_SIMPLE_VECTORSTORE_PGVECTOR_URL` 指定连接串，
    连接可达才执行。例如：

    ```bash
    podman run -d --name pgvector-pg17 -p 55432:5432 \
      -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
      docker.io/pgvector/pgvector:pg17

    OPENAI_SIMPLE_VECTORSTORE_PGVECTOR_URL='postgresql://postgres:postgres@localhost:55432/postgres' \
      python3 -m pytest test_engines_integration.py -q
    ```
  - `qdrant`：外部服务，安装 `qdrant-client` 且通过
    `OPENAI_SIMPLE_VECTORSTORE_QDRANT_URL` 指定的服务可达才执行。例如：

    ```bash
    podman run -d --name qdrant -p 6333:6333 -p 6334:6334 docker.io/qdrant/qdrant:latest

    OPENAI_SIMPLE_VECTORSTORE_QDRANT_URL='http://localhost:6333' \
      python3 -m pytest test_engines_integration.py -q
    ```

    > 未安装 `qdrant-client` 时，`test_engines_qdrant.py` 会注入内存 fake 客户端，
    > 离线即可完整覆盖 qdrant 引擎逻辑。
  - `redis`：外部服务，需带 RediSearch 的 redis-stack（或内置 search 模块的 Redis 8），
    通过 `OPENAI_SIMPLE_VECTORSTORE_REDIS_STACK_URL` 指定地址且 `FT._LIST` 可用才执行；
    普通 redis 会自动跳过。例如：

    ```bash
    podman run -d --name redis-stack -p 6379:6379 docker.io/redis/redis-stack:latest

    OPENAI_SIMPLE_VECTORSTORE_REDIS_STACK_URL='redis://localhost:6379/0' \
      python3 -m pytest test_engines_integration.py -q
    ```

```bash
# 真实嵌入式/外部后端（milvus-lite / sqlite-vec / pgvector / qdrant / redis，按运行时能力自动启用）
python3 -m pytest test_engines_integration.py -q
```

## 版本记录

### 0.3.1（2026-10-02）

- **索引生命周期收敛**：`ensure_*`（建表 / collection / index）改为每个引擎实例只探测
  一次（内部 `_index_ready` / `_table_ready`），读写热路径不再重复探测；新增
  `IndexDiff.exists` 区分"未创建"与"漂移"；新增 `check_index_fields` / `build_index_fields`
  按需补建后端原生过滤索引（显式 API，不在 ensure / 写入路径隐式触发）。
- **qdrant 落盘拆分可配**：向量 / HNSW 图 / payload 索引三部分是否落盘可分别配置
  （`qdrant_vector_on_disk` / `qdrant_hnsw_on_disk` / `qdrant_payload_on_disk`，
  `qdrant_on_disk` 为兜底总开关）；默认全部走内存（`False`，相对 0.3.0 的行为变更）。
- **统一检索错误诊断**：任一后端的向量检索（含超时、连接失败）抛出带后端地址 + 索引名 +
  检索参数的 `RuntimeError`，并用结构化 `index_missing` 标记只吞"真正未建索引"，避免按
  文案子串匹配误吞真实错误。
- **可配置超时**：redis / milvus / pgvector / elasticsearch / qdrant 新增连接与请求超时
  （`*_timeout` 参数或 `OPENAI_SIMPLE_VECTORSTORE_*_TIMEOUT`，默认 60 秒），redisvl 的
  `SearchIndex` 同样受 socket 超时约束。
- **跨引擎正确性修复**：
  - redis：`tag_multi` 改为直接以 TAG 分隔符拼接/拆分（RediSearch HASH 不解析反斜杠转义），
    `FT.INFO` 兼容 RESP2/RESP3 与 `dims=` / `DIM` 两种向量参数，`describe_index` 返回线上
    FT schema，`flush` glob 转义并清理缓存引擎，`PREFIX` 改为 `index_name + ":"` 以隔离
    同名前缀索引（旧索引需 `rebuild_index` 后生效）；
  - pgvector：共享表 `to_regclass` 为 NULL 视为"未建表"（不再建幻影表），每连接锁改用
    `WeakKeyDictionary` 安装，批量写入改为整批事务 + 失败回滚，`filter_expression` 字面 `%`
    转义；
  - sqlite-vec：整批写入持有连接锁并支持回滚，读/删不再隐式建表；
  - milvus / milvus-lite：含单引号的 `page_id` 回退到自建转义表达式，删除不再因可见性滞后
    短路，`describe_index` 缺失向量维度时逐级回退；
  - elasticsearch：`insert_many` 中途失败时回滚，覆盖写恢复批处理前旧文档；
  - qdrant：`filter_expression` 的 must/should/must_not 归一，numeric 等值过滤改用
    `Range(gte=v, lte=v)`。
- **过滤与字段模型加固**：`Filter` 取值归一（字符串不逐字符拆分、集合排序、接受单个
  `Filter`/`dict`），拒绝未声明字段、空值 / `None`、非法 `op` 与 `low > high`；numeric 取值
  统一转 `float`；`tag_multi` 全后端恒返回 list；核心 id 字段恒按字符串解码；
  `create_index(index_name, fields=...)` 保留与向量同名的显式声明并合并进实例声明。
- **打包**：`requires-python` 提升至 `>=3.10`，核心依赖补 `numpy` 并同步 `requirements.txt`，
  `pydantic>=2` 与各 extras 增加版本下界，entry-points 导入时自动注册。
- **审计回归（round4 / round5 / round6 / round7 / round8）**：新增
  `test_audit_round4.py` / `test_audit_round5.py` / `test_audit_round6.py` /
  `test_audit_round7.py` / `test_audit_round8.py`，分支覆盖率 100%。round5 / round6 另修复：
  redis 向量 `datatype` 配置生效且 `capabilities` 不再把 `text` 当作可过滤类型；
  elasticsearch `num_candidates` 封顶 10000；qdrant 无 `FloatIndexParams` 时 numeric 索引
  保持 float；sqlite-vec `ensure_table` 加锁并双重检查；并统一澄清 `flush` /
  `describe_index` / 引擎缓存等注释与 README 表述。round7 / round8 另修复：
  自定义 `document_schema` 不含标准分数字段时（README 允许只传子模型）检索排序 / rerank
  不再抛 `AttributeError` / `ValueError`（排序改用内部计算的分数，写入 rerank 分数时容忍
  缺失字段）；milvus-lite 不再在模块顶层硬导入 `pymilvus.milvus_client.index.IndexParams`
  （改为回退分支内惰性导入，并优先尝试旧签名），避免旧版 pymilvus 下整个后端 import 失败。

### 0.3.0（2026-09-24）

- 新增 **qdrant** 后端（原生 payload 过滤、`uuid5` point id 映射、可配置 `on_disk`，
  同时作用于向量、HNSW 图与 payload 索引），并补齐 `capabilities` / `describe_index` /
  `create_index` / 漂移检测与重建。
- `create_index(index_name, fields=...)` 语义统一：显式传入的 `fields` 优先于实例构造时的
  `declared_fields`，redis / elasticsearch / qdrant 均生效；已存在索引时按可增量方式补齐
  字段（redis `FT.ALTER ... SCHEMA ADD`、elasticsearch `put_mapping`、qdrant payload 索引）。
- 索引重建收敛为显式调用：`rebuild_index` 不再在创建 / ensure 时自动触发；新增只读的
  `compare_index_schema` 漂移检测（`force=False` 仅在漂移时重建）。
- 修复 redis 索引 schema 漂移误报：`FT.INFO` 向量参数兼容 `dims=` / `DIM` 键值对两种格式，
  字段类型与向量维度 / 距离度量归一化后比对。
- 移除未被调用的死钩子 `_prepare_engine`（milvus / milvus-lite / pgvector / elasticsearch /
  sqlite-vec）。
- 包元数据完善：描述列出全部后端并补充 embeddings / rerank 关键词。

### 0.2.0（2026-09-08）

- 演进为完整字段模型：新增 `IndexField` 声明（`tag` / `tag_multi` / `numeric` / `text` /
  `vector`）、`capabilities()` 能力矩阵、`describe_index()` 归一化 schema 查询、`create_index()`
  schema 声明建索引。
- 过滤 DSL 升级为统一的 `Filter`（含 `numeric` 区间 `op="range"`）并新增 `validate_filters`；
  各引擎 `_search_index` 收敛为 `(engine, query_embedding, index_name, filters, filter_expression, k)`，
  移除旧的 `kb_ids` / `categories` 参数（内部经 `Filter` 适配为 `vs_kb_id` / `vs_category` 字段）。
- redis / elasticsearch / milvus / milvus-lite / pgvector / sqlite-vec 全部适配新签名，
  并补齐 `capabilities` / `describe_index` / `_ensure_index_created`。
- `Document` 新增 `vs_metadata` 字段：检索结果携带业务自定义元数据（如声明 `IndexField`
  的可过滤标量 `vs_score`），非标准 `vs_*` 模型字段在 `_extract_custom_metadata` 中聚拢。
- milvus（独立部署）insert / delete 不再显式 `flush()`：写入后检索可见依赖服务端自动落盘
  （`cacheFlushInterval`，默认约 3s），有短暂写后读不一致窗口；集成测试据此改为有界轮询收敛。
- 覆盖率提升至 97%（阈值 90%）。

### 0.1.0（2026-09-08）

- 首个正式版本：在 redis-search 与 milvus 基础上新增 **pgvector**、**elasticsearch**、
  **sqlite-vec** 三个后端（factory + registry 注册，支持 extras 选择性安装）。
- 统一 embeddings / rerank 二阶段召回、插入 / 删除 / 更新（upsert）/ 查询 / 刷新等接口。
- 新增 kb / category 元数据过滤语义：各引擎在向量候选上按元数据过滤。
- milvus（独立部署）与 milvus-lite 支持同 page_id 更新（upsert）与删除后立即可见。
- elasticsearch 显式声明 kb / doc / category 等 keyword 映射以支持过滤，并自动迁移旧 schema。
- 新增对 pgvector / elasticsearch / sqlite-vec / milvus 真实外部服务的端到端测试
  （`test_engines_integration.py`），覆盖率 96%（阈值 90%）。

## License

[MIT](LICENSE)
