RAG 在实际应用中,检索环节到底存在哪些“坑”?一个典型的场景是:精确匹配特定标识符(ID)。很多业务数据都带有唯一 ID,比如包含数字和字母的复杂组合,长度轻轻松松超过 10 个字符。这时候,如果只依赖基于向量的语义搜索(比如用 PGVector 存嵌入向量做相似性检索),结果往往差强人意。
语义搜索靠的是向量之间的软相似性来排序——计算余弦相似度,返回前 k 个最接近的文本块。处理语义相近的文本时确实好用,但碰上 ID 这种需要严格一致的活儿,就不灵了。举个具体例子:查询“1234567890XYZ”时,语义搜索可能把“1234567890ABC”排在前面,因为它俩在向量空间里距离不远;真正要的那个反而因为嵌入时的细微差异被挤到后面,甚至直接出局。这个问题在医疗记录查询(根据患者 ID 查病历)、金融交易追溯(按交易 ID 拿详情)等场景里,后果相当严重——检索结果错了,后续生成就全偏了。
怎么破?混合搜索是一条切实可行的路——把多种搜索技术组合起来,取长补短。具体分几步走:
基于 PGVector 的语义搜索
PGVector 是 PostgreSQL 里专门存向量和做相似性查询的扩展。第一步用它做语义搜索,能快速找到和查询语义相近的文本块。可以选一个合适的嵌入模型(比如 JinaAI 的“jina-embeddings-v2-base-en”),把文档和查询都转成向量,然后在数据库里高效检索。比如在新闻文章库里搜“科技行业最新动态”,PGVector 能迅速返回语义上相关的片段。但正如前面说的,它对精确 ID 匹配力不从心,所以需要其他搜索来补位。
PostgreSQL 全文搜索
这一步专门对付精确匹配和部分匹配。PostgreSQL 自带的全文搜索功能很强,正好弥补语义搜索的短板。
- 精确匹配:用
to_tsvector和plainto_tsquery函数,确保查询的 ID 在文本块里一个字符都不差地出现。比如查“1234567890XYZ”,to_tsvector('english', content) @@ plainto_tsquery('english', '1234567890XYZ')这条语句能在 documents 表的 content 字段里找到完全匹配的记录。 - 部分匹配:如果精确匹配没结果,就用
ILIKE操作符做模糊匹配,例如content ILIKE '%1234567890XYZ%'。这样能处理 ID 带拼写错误或格式变体的情况,尽量扩大召回范围。
合并结果
语义搜索和全文搜索的结果可能有重叠,需要去重。具体实现上,在 Python 里可以用集合或字典来合并,按文档 ID 或内容去重。合并后的集合既包含语义相关的,也包含文本精确匹配的,给后续重排序提供更全面的素材。
使用 Flash Re-Ranker 重排序
混合搜索拿到的结果,顺序不一定最优——最相关的可能排在后面。Flash Re-Ranker 就是干这个的:它用更复杂的算法,综合考虑语义、词汇匹配度、上下文等因素,对合并后的结果重新排序,把最相关的文本块推上前列。比如在产品描述库里搜特定产品 ID,它能把精确命中并且语义最匹配的描述排在最前面。
基于 LangChain 的实现
LangChain 这个框架很适合整合检索和文本处理流程。下面是一套完整的实现示例:
(一)基于 PGVector 的语义搜索实现
from langchain.vectorstores import PGVector
from jinaai import Embedding
# 初始化PGVector
vector_store = PGVector(
connection_string="postgresql://myuser:mypassword@localhost/mydb",
embedding_function=Embedding(model="jina-embeddings-v2-base-en")
)
# 执行语义搜索
query = "1234567890XYZ"
semantic_results = vector_store.similarity_search(query, k=10)
print(semantic_results)
代码中先用 PGVector 类创建向量存储对象,指定连接字符串和嵌入函数,然后用 similarity_search 方法做语义搜索,k 控制返回的结果数量。
(二)PostgreSQL 全文搜索实现
import psycopg2
# 定义自定义停用词
STOP_WORDS = {
"a", "an", "and", "are", "as", "at", "be", "but", "by", "for", "if",
"in", "into", "is", "it", "no", "not", "of", "on", "or", "such", "that",
"the", "their", "then", "there", "these", "they", "this", "to", "was",
"will", "with", "me", "my", "you", "your", "we", "our", "us", "he",
"him", "his", "she", "her", "hers", "it", "its", "them", "so", "too"
}
def filter_stop_words(query):
"""从输入查询中去除停用词。"""
words = query.split()
filtered_words = [word for word in words if word.lower() not in STOP_WORDS]
return " ".join(filtered_words) if filtered_words else query
def perform_full_text_search(query):
conn = psycopg2.connect("dbname=mydb user=myuser password=mypassword host=localhost")
cursor = conn.cursor()
filtered_query = filter_stop_words(query)
print(f"原始查询: {query}")
print(f"过滤后的查询(不含停用词): {filtered_query}")
# 精确匹配(过滤停用词后)
cursor.execute(
"SELECT * FROM documents WHERE to_tsvector('english', content) @@ plainto_tsquery('english', %s);",
(filtered_query,)
)
exact_results = cursor.fetchall()
# 部分匹配(ILIKE,不过滤停用词)
cursor.execute("SELECT * FROM documents WHERE content ILIKE %s;", (f"%{query}%",))
partial_results = cursor.fetchall()
cursor.close()
conn.close()
return exact_results + partial_results
query = "I want to see details of 1234567890XYZ"
retrieved_chunks = perform_full_text_search(query)
print("结果:", retrieved_chunks)
这段代码先定义停用词集合,写了一个过滤函数 filter_stop_words,然后 perform_full_text_search 函数里分别做精确匹配(过滤停用词后)和部分匹配(ILIKE),最后合并返回。
(三)合并和重排序结果
from flashrank import FlashRanker
def re_rank_results(semantic_results, text_results):
ranker = FlashRanker()
semantic_texts = [{"text": doc.page_content} for doc in semantic_results]
text_texts = [{"text": row[1]} for row in text_results] # 假设文本在第二列
combined_results = semantic_texts + text_texts
reranked_results = ranker.rank(combined_results)
return reranked_results
final_results = re_rank_results(semantic_results, retrieved_chunks)
print(final_results)
re_rank_results 函数提取语义和文本搜索的结果,合并后调用 Flash Re-Ranker 的 rank 方法完成重排序。
重要考虑因素
整个流程里有几个细节值得注意:
自定义停用词
全文搜索里停用词处理很关键。停用词是那些出现频率高但对检索没帮助的词(a、the、and 等)。自定义停用词列表可以根据场景灵活调整:科技文献里某些常用词可能很有意义,就需要移除;代码注释里则可能要把特殊符号加进去。合理的停用词配置能明显提升检索准确率。
PostgreSQL 配置
PostgreSQL 内置了多种语言的停用词列表,还支持创建自定义词典来实现更精细的处理。根据文本语言和应用场景选择合适的内置列表,或者建自定义词典来标记特殊词汇或做词形转换,能进一步优化全文搜索性能。
性能优化
去除停用词不仅能提高准确率,还能加快搜索速度——尤其是在处理大规模数据集时。去掉那些低频词汇后,数据库要处理的词数减少,索引也可以更小更高效。实际应用中需要根据数据集规模和特性,在停用词处理复杂度和性能提升之间做好平衡。
回到检索精度这个核心问题:通过 PGVector 语义搜索 + PostgreSQL 全文搜索 + Flash Re-Ranker 重排序的混合方案,能有效解决特定 ID 匹配不准确的老大难问题,让 RAG 系统在真实业务场景中跑得更稳、更准。
