背景与目标
自用一个博客类型网站,文本数据都在 posts 表中,需要实现全文搜索,以前使用的是方案是使用 分词加上 Elasticsearch 索引与搜索,现在想换个新的方式来做这个文章搜索。
现在已经进入 AI 时代,那 AI 方法如何来完成这个搜索任务?上次看到的一种方式是使用 GPT 模型做向量搜索,将文本数据转换为向量,然后使用向量数据库来实现搜索。
使用 GPAI 实现全文搜索
网站使用的数据库是 postgres,实现的方式也很多,本次选用 Timescaledb 的 gpai 扩展来实现向量搜索。
什么是 gpAI
项目地址:timescale/pgai,官方的定义是,一套工具,可使用 PostgreSQL 更轻松地开发 RAG、语义搜索和其他 AI 应用程序。也就是在 postgresql 中直接使用扩展来扩展 AI 能力。
大概看了下官方文档,pgAI 扩展主要提供了以下功能:
- 文本嵌入:使用 OpenAI 的 Embedding 模型将文本转换为向量。
- 数据摄取:通过 pgai 扩展将数据转换为向量并存储在 PostgreSQL 中。
- 数据查询:通过 pgai 扩展 库执行高效的相似性搜索查询。
首先弄清楚几个问题
- 为什么需要使用 OpenAI 的 Embedding 模型?
- 生成向量,pgAI 使用 OpenAI 的 Embedding 模型用于将文本转换为嵌入向量,这些向量是文本的数学表示,通常用于计算文本之间的相似度。
- 相似性搜索,在进行相似性搜索时,pgAI 会使用 Embedding 模型再次生成向量,然后遍历所有已经存在的向量与本次查询的向量做相似度比较,按照相似度高低进行排序。排在前列的文本内容就是最相似的。
- openai Embedding 模型,调用完毕生成了什么?
OpenAI 的 Embedding 模型用于将文本转换为嵌入向量,这些向量是文本的数学表示,通常用于计算文本之间的相似度。调用 Embedding 模型后,生成的主要结果是一个浮点数数组,表示输入文本的嵌入。
- 向量数据如何在搜索时发挥作用?
先将文本内容生成的向量存入到数据库中,在搜索时,将搜索的内容也生成向量,然后遍历所有已经存在的向量与本次查询的向量做相似度比较,按照相似度高低进行排序。排在前列的文本内容就是最相似的。
- 如何将已经存在的表的文本内容做成向量存储到数据库中?
创建向量化器,在特定表文本内容更新时插入到向量表中
SELECT ai.create_vectorizer( 'plan'::regclass, destination => 'plan_embeddings', embedding => ai.embedding_openai('text-embedding-ada-002', 768), chunking => ai.chunking_recursive_character_text_splitter('content') );
这里的 plan 是表名,plan_embeddings 是向量表名,text-embedding-ada-002 是 openai 的 embedding 模型,768 是向量维度。
- openai 的向量模型,本地ollama 模型,二者生成向量数据有区别吗?
- 模型性能
- OpenAI 的嵌入模型(如 text-embedding-3-large 和 text-embedding-3-small)被广泛认为在检索任务中表现优异,尤其是在处理复杂的语义关系时。许多用户和开发者反馈称,OpenAI 的嵌入模型在相似性搜索和信息检索方面的效果明显优于当前可用的 Ollama 嵌入模型。
- Ollama 模型虽然是开源的,但在某些情况下,其性能可能不如 OpenAI 的商业模型,尤其是在处理特定领域的数据时。
- 模型效果
- 在处理复杂的语义关系时,OpenAI 的嵌入模型表现更好。
- Ollama 模型在处理特定领域的数据时,可能会有更好的表现,但整体上,用户普遍认为其在检索效果上不及OpenAI。
- 使用场景
- OpenAI 的嵌入模型通常通过 API 访问,这意味着需要网络连接和 API 密钥,并且可能会涉及费用。这使得它们适合需要高性能和高准确性的商业应用。
- Ollama 模型是本地运行的,不需要网络连接,也不涉及 API 密钥,因此更适合对隐私和数据安全有较高要求或希望避免额外费用的场景。
- 如何比较
- 可以使用多种 AI 模型,来生成 embeddings,存储到数据库中,分别进行搜索,对比看看效果。
- 由谁来负责相似度计算与对比?
pgAI负责相似度对比:一旦生成了嵌入向量,pgAI会在PostgreSQL数据库中使用这些向量进行相似度计算。pgAI利用内置的功能(如向量距离计算)来比较不同文本之间的相似性。例如,可以使用余弦相似度或欧几里得距离等方法来完成这一任务。
集成与性能提升:pgAI将嵌入生成和相似度计算整合到数据库中,使得用户能够在同一环境下进行数据存储、处理和分析。这种集成简化了开发流程,并提高了性能,避免了将数据传输到外部服务进行处理的延迟。
SELECT content, embedding <=> ai.openai_embed('text-embedding-ada-002', '您的查询文本') AS distance FROM plan_embeddings ORDER BY distance LIMIT 5;
在这个查询中,<=>运算符用于计算存储在 embedding 列中的向量与输入文本生成的嵌入之间的距离,从而实现相似度对比。
开始操作
以下是记录的操作过程,方便后续回顾与分享。
检查 pgAI 扩展能否使用
- 连接数据库,
psql -d "postgres://<username>:<password>@<host>:<port>/<database-name>"
,如果是本机操作,切换账号 psql 操作。 - 启用 pgai 扩展,
CREATE EXTENSION IF NOT EXISTS ai CASCADE;
- 错误信息:
DETAIL: Could not open extension control file "/usr/share/pgsql/extension/ai.control": No such file or directory.
,表示系统安装的 psql 中,没有带上这个扩展;要么使用 timescaledb docker,要么使用 pgvector 扩展。
timescale docker 安装
指导文件,向量器安装,按照这个文件,使用 timescaledb,自带了 ai 能力。关键是如何将本机数据表数据弄到docker pgsql 中做实验。
确保系统中文件足够,因为 docker image 文件通常很大
操作步骤
- 导入原有数据
- 安装、启动 docker 服务,不再赘述。
- 本机数据备份
- pg_dump 备份数据。
- 启动 docker
- 新建文件夹,新建配置文件,docker-compose.yml,加入上面链接中的内容;
- 这里有个坑,回来记录下。
name: pgai services: db: image: timescale/timescaledb-ha:pg17 user: root # 这里也是一个坑,必须指定用户为 root,否则 ./data(这个文件在宿主机上,docker 中使用的用户在宿主机上没有,所以需要 root,让 docker 自己修改文件夹所有权) 因为权限问题,无法启动; environment: POSTGRES_PASSWORD: postgres PGDATA: /var/lib/postgresql/data # 这里也是一个坑,这里的位置必须手动指定,否则放在 /home/postgres 中,导致映射有问题;这里的路径需要和下方 volumes 的路径一致; OPENAI_BASE_URL: xx # 如果使用 openai 的 embedding 模型,需要指定 base url;默认值不需要添加此项 OPENAI_API_KEY: xx # 如果使用 openai 的 embedding 模型,需要指定 api key; ports: - "5432:5432" volumes: - ./data:/var/lib/postgresql/data # 这里也是一个坑,路径需要和上方 PGDATA 的路径一致; vectorizer-worker: image: timescale/pgai-vectorizer-worker:v0.2.1 environment: PGAI_VECTORIZER_WORKER_DB_URL: postgres://postgres:postgres@db:5432/postgres # 注意这里,尾巴上的postgres,是数据库的名称,注意修改; OLLAMA_HOST: http://ollama:11434 OPENAI_BASE_URL: xx # 如果使用 openai 的 embedding 模型,需要指定 base url;默认值不需要添加此项 OPENAI_API_KEY: xx # 如果使用 openai 的 embedding 模型,需要指定 api key; command: [ "--poll-interval", "5s" ] ollama: image: ollama/ollama
- 下载、启动服务,
docker compose up -d
- 导入数据
- 复制 sql 文件到容器中,
docker cp <path-to-backup-file> <container-id>:/tmp/backup.sql
- 进入 docker bash,
docker exec -it pgai-db-1 /bin/bash
- 如果备份的数据中,用到了角色或者赋权等行为,需要在 docker 中创建角色,
CREATE ROLE <role_name> WITH LOGIN PASSWORD 'your_password';
- 执行导入命令,
pg_restore -U postgres -C -d postgres /tmp/backup.sql
- 打开 ai 能力
- 连接 docker 中数据库,
docker exec -it pgai-db-1 psql -U postgres
- 启用 pgai 扩展,
CREATE EXTENSION IF NOT EXISTS ai CASCADE;
- 检查扩展是否加载成功,
SELECT * FROM pg_extension;
或者\dx
- 打开日志,检查向量器是否在工作
- 看日志,
docker compose logs -f vectorizer-worker
本地模型(使用 Ollama 实现)
- 下载 embedding 模型
- 在docker 中下载本地模型,
docker compose exec ollama ollama pull nomic-embed-text
- 创建向量器
- 切换 schema,视情况而定,
SET search_path TO blog_1kcode;
- 创建向量器,
sql set search_path to blog_1kcode,public; # 没有额外指定 schema,此句省略,下同; SELECT ai.create_vectorizer( 'posts'::regclass, destination => 'posts_content_embeddings', embedding => ai.embedding_ollama('nomic-embed-text', 768), chunking => ai.chunking_recursive_character_text_splitter('content_zh') );
- 不用指定 schema,destination,自动使用 source 中的 schema 了;这里的 content 是栏位名称;
- 检查与查询
sql set search_path to blog_1kcode,public; SELECT * FROM posts_content_embeddings; # 检查向量是否存在。 SELECT chunk, embedding <=> ai.ollama_embed('nomic-embed-text', 'good food', host => 'http://ollama:11434') as distance FROM posts_content_embeddings ORDER BY distance; # 查询功能是否正常
- 备注
- 这种方式,会调用 vps本地 cpu,去计算生成 embeddings,导致 cpu 100%,放弃;
使用 openai 的 embedding 模型
- 创建向量器
set search_path to blog_1kcode,public; SELECT ai.create_vectorizer( 'posts'::regclass, destination => 'posts_content_embeddings', embedding => ai.embedding_openai('text-embedding-3-small', 768), chunking => ai.chunking_recursive_character_text_splitter('content_zh') );
- 检查与查询
sql set search_path to blog_1kcode,public,ai; SELECT chunk, embedding <=> ai.openai_embed('text-embedding-3-small', 'UX 最新的设计趋势', dimensions=>768) as distance FROM posts_content_embeddings ORDER BY distance;
其他问题
- 错误信息:
operator does not exist: public.vector <=> public.vector at character 41 HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
- 解决方法:
set search_path to blog_1kcode,public;
,因为 vector 在 public schema下,所以需要切换 schema;
- 解决方法:
- 如何修改 openai 的 base url?
- docker 配置修改,直接添加环境变量, OPENAI_BASE_URL 与 OPENAI_API_KEY;
- 不要想着替换 base_url和 key,能够用上 openrouter中充值的钱了,做不到,openrouter 不支持 embeddings 模型;
- embedding_ollama 支持修改 base_url,和 key,不知道能否使用其他类型的 embeddings 模型;需要再测试;
- 会在启用 ai 的数据库下,创建一个 schema,默认是 ai,如果需要指定 schema,需要使用
SET search_path TO <schema>;
切换 schema。ai 使用的表都在这里; - ** PGSQL 中执行出错**
- 权限的问题,比如连接用户,不是超级用户时,需要一些赋权操作;因为 ai 操作的函数,在 schema ai 中,需要赋权。
- openai_embed 参数传递不能用双引号,需要使用单引号,否则提示没这个字段的错误。
- 遇到不能正常工作的问题,检查下配置,比如数据库名称是否对上了,是否切换了 schema,等等;
- docker 配置项检查
docker inspect pgai-db-1
- docker 启动、运行日志
docker logs --tail 100 <container_name>
- javascript 代码中查询不到数据?
- 使用的 @vecel/postgres,连接不上数据库,存在问题,不用;使用 pg 库就行;
- 导入的数据库被删除,main_site 被删除
- docker logs `LOG: TimescaleDB background worker scheduler for database 28404 will be stopped`,STATEMENT: DROP DATABASE main_site;
- 前面怀疑是因为系统 cpu 占用太高,自动给删除了,使用 openai 模型后,问题依旧;
- 是否是 timescaledb 的后台任务,看后台任务像是日志上报,没有问题;
- 日志中还有删除其他数据库的操作,突然想到,应该是默认密码,且暴露在外网上导致问题。以前安装的版本是不允许postgres 用户远程登录的。这次应该是安全问题;再次查看数据库中数据,发现一个 readme_to_recover 的数据库,查看里面的 readme 数据表,发现这么一段话:All your data is backed up. You must pay 0.0040 BTC to bc1qaphucv42gk89frjkf357nl636r7qjrqvavneu0 In 48 hours,。。
- 原来是有人恶意攻击,重新导入数据,修改密码后,再观察。