104-实战论文搜索引擎-ArXiv爬取-Milvus存储-RAG问答-Gradio前端

文章目录

  • 【104.Python+AI】实战:搭建"论文搜索引擎"------10万篇论文,输入问题,秒出相关段落
    • 导入语
    • [1 ~> 整体架构:六步流水线](#1 ~> 整体架构:六步流水线)
    • [2 ~> 第一步:ArXiv 数据获取](#2 ~> 第一步:ArXiv 数据获取)
    • [3 ~> 第二步:分块------摘要场景的"反常识"选择](#3 ~> 第二步:分块——摘要场景的"反常识"选择)
    • [4 ~> 第三步:向量化入库](#4 ~> 第三步:向量化入库)
      • [4.1 模型与批量优化](#4.1 模型与批量优化)
      • [4.2 Milvus 集合设计](#4.2 Milvus 集合设计)
    • [5 ~> 第四步:三级检索流水线](#5 ~> 第四步:三级检索流水线)
    • [6 ~> 第五步:RAG 问答增强](#6 ~> 第五步:RAG 问答增强)
    • [7 ~> 第六步:Gradio 前端,30行收工](#7 ~> 第六步:Gradio 前端,30行收工)
    • [思考 && 总结](#思考 && 总结)
    • 结尾

【104.Python+AI】实战:搭建"论文搜索引擎"------10万篇论文,输入问题,秒出相关段落

📖 文章简介: 本文是向量数据库板块的收官实战,把前九篇学过的零件------Embedding、分块、ANN索引、Milvus、RAG------组装成一个完整可用的论文搜索引擎:10万篇ArXiv论文入库,输入自然语言问题,秒级返回相关段落并附出处。文章按真实开发顺序展开六步流水线:ArXiv数据获取(官方API批量拉取元数据+摘要,速率限制与断点续传)、分块策略(按摘要+章节结构的语义分块,Chunk Size选择依据)、向量化入库(BGE中文/英文模型选择、批量Embedding的吞吐优化、Milvus集合Schema设计)、检索服务(查询改写+向量检索+Rerank精排的三级流水线)、RAG问答增强(检索段落拼入Prompt让LLM生成带引用的答案)、Gradio前端(30行代码的问答界面+出处展示+一键部署)。每一步给出可直接运行的关键代码与踩坑提示,配以Mermaid流程图展示整体架构,适合想动手把向量检索技术串成完整项目的开发者阅读参考。


🎬 个人主页: 源码骑士

专栏传送门: 《Android开发基础》《python基础课程》

⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂


🎬 源码骑士的简介:

5年Android Framework系统开发经验,曾主导多项系统级性能优化专项

技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)

累计产出原创技术文章100+篇,文章以流程图为特色,被读者评价为"看一篇胜过啃一周源码"


导入语

前面九篇,我们把向量检索的零件逐个打磨了一遍:Embedding是地基、ANN是引擎、Milvus是车架、调优和治理是保养手册。零件再好,不组装起来永远只是一堆零件。

这篇就来总装。目标产品:一个论文搜索引擎------10万篇ArXiv论文进库,你用大白话提问("有没有用强化学习优化推荐系统的最新方法"),它秒级返回最相关的论文段落,附上标题、作者、原文链接,还能让大模型基于这些段落给你一段带引用的综述式回答。

选论文搜索作为实战,是因为它的数据获取完全合法免费、文本结构规整、又足够真实------10万条的规模刚好踩到"单机Milvus需要认真配置"的门槛。跟着做完,你手里就是一个可以改造成任意垂直搜索的完整骨架。


1 ~> 整体架构:六步流水线

#mermaid-svg-6F546LCpMICtUwEA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6F546LCpMICtUwEA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6F546LCpMICtUwEA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6F546LCpMICtUwEA .error-icon{fill:#552222;}#mermaid-svg-6F546LCpMICtUwEA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6F546LCpMICtUwEA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6F546LCpMICtUwEA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6F546LCpMICtUwEA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6F546LCpMICtUwEA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6F546LCpMICtUwEA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6F546LCpMICtUwEA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6F546LCpMICtUwEA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6F546LCpMICtUwEA .marker.cross{stroke:#333333;}#mermaid-svg-6F546LCpMICtUwEA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6F546LCpMICtUwEA p{margin:0;}#mermaid-svg-6F546LCpMICtUwEA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6F546LCpMICtUwEA .cluster-label text{fill:#333;}#mermaid-svg-6F546LCpMICtUwEA .cluster-label span{color:#333;}#mermaid-svg-6F546LCpMICtUwEA .cluster-label span p{background-color:transparent;}#mermaid-svg-6F546LCpMICtUwEA .label text,#mermaid-svg-6F546LCpMICtUwEA span{fill:#333;color:#333;}#mermaid-svg-6F546LCpMICtUwEA .node rect,#mermaid-svg-6F546LCpMICtUwEA .node circle,#mermaid-svg-6F546LCpMICtUwEA .node ellipse,#mermaid-svg-6F546LCpMICtUwEA .node polygon,#mermaid-svg-6F546LCpMICtUwEA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6F546LCpMICtUwEA .rough-node .label text,#mermaid-svg-6F546LCpMICtUwEA .node .label text,#mermaid-svg-6F546LCpMICtUwEA .image-shape .label,#mermaid-svg-6F546LCpMICtUwEA .icon-shape .label{text-anchor:middle;}#mermaid-svg-6F546LCpMICtUwEA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6F546LCpMICtUwEA .rough-node .label,#mermaid-svg-6F546LCpMICtUwEA .node .label,#mermaid-svg-6F546LCpMICtUwEA .image-shape .label,#mermaid-svg-6F546LCpMICtUwEA .icon-shape .label{text-align:center;}#mermaid-svg-6F546LCpMICtUwEA .node.clickable{cursor:pointer;}#mermaid-svg-6F546LCpMICtUwEA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6F546LCpMICtUwEA .arrowheadPath{fill:#333333;}#mermaid-svg-6F546LCpMICtUwEA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6F546LCpMICtUwEA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6F546LCpMICtUwEA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6F546LCpMICtUwEA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6F546LCpMICtUwEA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6F546LCpMICtUwEA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6F546LCpMICtUwEA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6F546LCpMICtUwEA .cluster text{fill:#333;}#mermaid-svg-6F546LCpMICtUwEA .cluster span{color:#333;}#mermaid-svg-6F546LCpMICtUwEA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6F546LCpMICtUwEA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6F546LCpMICtUwEA rect.text{fill:none;stroke-width:0;}#mermaid-svg-6F546LCpMICtUwEA .icon-shape,#mermaid-svg-6F546LCpMICtUwEA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6F546LCpMICtUwEA .icon-shape p,#mermaid-svg-6F546LCpMICtUwEA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6F546LCpMICtUwEA .icon-shape .label rect,#mermaid-svg-6F546LCpMICtUwEA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6F546LCpMICtUwEA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6F546LCpMICtUwEA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6F546LCpMICtUwEA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 在线检索管道
离线建库管道

否, 要综述
ArXiv API

批量拉取论文
分块

摘要+章节语义切分
BGE Embedding

批量向量化
Milvus

10万+向量
用户自然语言问题
查询改写

LLM润色检索词
向量检索

HNSW Top-50
Rerank精排

Top-5
只要段落?
直接返回段落+出处
段落拼入Prompt

LLM生成带引用回答
Gradio界面展示

答案+引用列表

建库管道跑一次(或增量跑),检索管道每次查询都跑------两条管道唯一的交汇点是Milvus。这个"读写分离"的结构是所有搜索系统的标准骨架。


2 ~> 第一步:ArXiv 数据获取

ArXiv提供官方API,不用写爬虫(爬虫那点事留给别的项目):

python 复制代码
# pip install arxiv
import arxiv
import json
import time

def fetch_papers(query="cat:cs.AI OR cat:cs.CL OR cat:cs.LG",
                 total=100_000, batch=2000):
    client = arxiv.Client(page_size=batch, delay_seconds=3, num_retries=3)
    search = arxiv.Search(query=query, max_results=total,
                          sort_by=arxiv.SortCriterion.SubmittedDate)
    with open("papers.jsonl", "a", encoding="utf-8") as f:
        for i, paper in enumerate(client.results(search)):
            f.write(json.dumps({
                "arxiv_id": paper.entry_id.split("/")[-1],
                "title": paper.title,
                "abstract": paper.summary.replace("\n", " "),
                "authors": [a.name for a in paper.authors],
                "published": paper.published.isoformat(),
                "pdf_url": paper.pdf_url,
            }, ensure_ascii=False) + "\n")
            if (i + 1) % batch == 0:
                time.sleep(3)          # 尊重API速率限制

三个坑先排掉:

bash 复制代码
坑一:速率限制。delay_seconds=3 是官方礼仪下限,压得太狠会封IP
坑二:断点续传。10万条要跑几小时,jsonl追加写+记录已抓ID,
     中断后跳过已抓的继续
坑三:本实战只用摘要(abstract)入库。全文PDF解析成本高,
     摘要已含论文核心贡献------先跑通,再考虑全文增强

3 ~> 第二步:分块------摘要场景的"反常识"选择

第46篇讲过Chunking的艺术,但这个项目里有个反常识的结论:摘要场景不要切碎。

bash 复制代码
论文摘要 ≈ 150~300词,本身就是一个完整的语义单元
→ 再切分只会破坏语义完整性
→ 正确策略:一条摘要 = 一个Chunk

想增强检索覆盖面的进阶做法:
  Chunk = title + abstract 拼接(标题携带关键词信号)
  长论文全文版才需要按章节切分(每章一个Chunk,
  元数据里带 chapter 字段)

教训是通用的:分块策略跟着数据结构走,不是跟着教程走。 先看你的一条数据长什么样,再决定切不切。


4 ~> 第三步:向量化入库

4.1 模型与批量优化

英文论文库选英文向量的强项模型;批量Embedding是建库速度的关键:

python 复制代码
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-large-en-v1.5")

def embed_batch(texts):
    return model.encode(
        texts,
        batch_size=64,               # GPU上可到128~256,CPU调到16~32
        normalize_embeddings=True,   # 归一化后内积=余弦相似度
        show_progress_bar=True,
    ).tolist()

10万条摘要在一张消费级GPU上约20~40分钟跑完;纯CPU的话扔过夜任务即可------建库是一次性成本,不必为它买高端卡。

4.2 Milvus 集合设计

python 复制代码
from pymilvus import MilvusClient, DataType

client = MilvusClient(uri="http://localhost:19530")
schema = client.create_schema(auto_id=False)
schema.add_field("chunk_id",  DataType.INT64, is_primary=True)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1024)  # bge-large是1024维
schema.add_field("title",     DataType.VARCHAR, max_length=512)
schema.add_field("chunk_text", DataType.VARCHAR, max_length=4096)
schema.add_field("authors",   DataType.VARCHAR, max_length=1024)
schema.add_field("year",      DataType.INT32)                    # 按年份过滤用
schema.add_field("arxiv_id",  DataType.VARCHAR, max_length=64)   # 出处溯源用

client.create_collection("papers", schema=schema)

index = client.prepare_index_params()
index.add_index(field_name="embedding", index_type="HNSW",
                metric_type="COSINE",
                params={"M": 32, "efConstruction": 200})   # 第96/97篇的参数结论
client.create_index("papers", index_params=index)
client.load_collection("papers")

注意chunk_text直接存原文------检索命中后不用再回查源文件,向量库本身就是文档库,省掉一次外部存储往返。


5 ~> 第四步:三级检索流水线

python 复制代码
from openai import OpenAI
llm = OpenAI()

def search_papers(question: str, top_k: int = 5):
    # 一级:查询改写------把大白话改写成学术检索词
    refined = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content":
                   f"把下面的问题改写成适合检索英文学术论文的关键词句式,"
                   f"只输出改写结果:{question}"}],
    ).choices[0].message.content

    # 二级:向量检索,取Top-50
    q_vec = embed_batch([refined])[0]
    candidates = client.search(
        collection_name="papers", data=[q_vec], limit=50,
        search_params={"params": {"ef": 128}},
        output_fields=["title", "chunk_text", "authors", "year", "arxiv_id"],
    )[0]

    # 三级:Rerank精排,留Top-5(bge-reranker,第50篇)
    from sentence_transformers import CrossEncoder
    reranker = CrossEncoder("BAAI/bge-reranker-large")
    pairs = [(question, c["entity"]["chunk_text"]) for c in candidates]
    scores = reranker.predict(pairs)
    ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
    return [c for c, _ in ranked[:top_k]]

为什么三级都要:改写解决"用户不会问",向量解决"找得全",Rerank解决"排得准"------缺一级,最终质量掉一档。


6 ~> 第五步:RAG 问答增强

只要段落,上一步结果就是终点;要综述式回答,把段落喂给LLM:

python 复制代码
def answer(question: str):
    hits = search_papers(question)
    context = "\n\n".join(
        f"[{i}] {h['entity']['title']} ({h['entity']['year']})\n"
        f"{h['entity']['chunk_text']}"
        for i, h in enumerate(hits, 1)
    )
    resp = llm.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content":
             "你是学术助手。仅基于给出的论文段落回答问题,"
             "引用处用[编号]标注,段落里没有的信息明确说不知道。"},
            {"role": "user", "content": f"论文段落:\n{context}\n\n问题:{question}"},
        ],
    )
    return resp.choices[0].message.content, hits   # 答案 + 出处列表

[编号]引用 + "没有就说不知道"------这两条Prompt约束是抑制幻觉的最低配置(第128篇会系统展开)。


7 ~> 第六步:Gradio 前端,30行收工

python 复制代码
# pip install gradio
import gradio as gr

def chat_ui(message, history):
    answer_text, hits = answer(message)
    refs = "\n\n".join(
        f"**[{i}]** {h['entity']['title']} ({h['entity']['year']})  \n"
        f"https://arxiv.org/abs/{h['entity']['arxiv_id']}"
        for i, h in enumerate(hits, 1)
    )
    return answer_text + "\n\n---\n**参考论文:**\n\n" + refs

demo = gr.ChatInterface(
    fn=chat_ui,
    title="论文搜索引擎",
    description="10万篇ArXiv论文 · 输入问题 · 秒出相关段落",
    examples=["有没有用强化学习优化推荐系统的最新方法?",
              "RAG和微调哪个更适合企业知识库?"],
)
demo.launch(share=True)     # share=True 生成公网链接

到此全链路闭环。后续迭代的优先顺序建议:先补全文解析(摘要信息毕竟有限),再加年份/作者过滤(元数据字段已留好),最后考虑第100篇的Graph RAG(论文引用关系天然是图)。


思考 && 总结

  1. 骨架是读写分离: 离线建库管道跑一次,在线检索管道每次查询跑,两管道只在Milvus交汇------这是所有搜索系统的标准结构。
  2. 数据获取讲礼仪: 官方API+限速+断点续传;先用摘要跑通,全文解析放二期。
  3. 分块跟着数据结构走: 摘要本身是完整语义单元,不切;title+abstract拼接白捡一层检索信号。
  4. 检索三级缺一不可: 查询改写管"问得对"、向量管"找得全"、Rerank管"排得准"。
  5. RAG的最低幻觉配置: 引用编号 + "不知道就说不知道";向量库存原文省一次回查。

板块实战到此结束。从下一篇开始,我们换一条主线------不再调用别人的API,而是把大模型搬回自己机器:Ollama一行命令本地跑Llama3、Qwen2,开启本地部署与推理优化板块。


结尾

各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!

源码骑士 --- Android Framework & 全栈开发

👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长

❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量

收藏:把核心知识点存好,在需要时随时查、随时用

💬 评论:分享你的经验或疑问,评论区一起交流避坑

🔄 一键四连:不要忘记给博主"一键四连"哦!

🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向

结语:十个零件、六步管道、一个能用的搜索引擎------技术的价值终究要在组装中兑现。这个项目跑通的那一刻,向量检索对你而言就再也不是概念,而是手里的一把锤子。不要忘记给博主"一键四连"哦!

相关推荐
比高创意品牌策划设计1 小时前
零售卖场门头设计怎么做才显眼
python
sunly_1 小时前
TypeScript总结:15、类型速查
前端·javascript·typescript
Brown.alexis1 小时前
es6知识点3-自备使用
前端·javascript·es6
2601_953988071 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·后端·物联网·tcp/ip·数学建模·前端框架
天空之城--1 小时前
Claude Code 高效开发 Web 2D/3D 完全指南:心法、自定义 Skill 体系与社区技能包实战
前端·3d
IT_陈寒1 小时前
SpringBoot自动配置坑了我三天,原来漏了这个注解
前端·人工智能·后端
鬼手点金1 小时前
Scrapy + Playwright 完整示例(JS 动态渲染网页)
开发语言·javascript·爬虫·python·scrapy·html·json
存在morning1 小时前
【Python 开发实践 一】Python vs Go vs Java 三门语言对比
java·python·golang
vx-程序开发1 小时前
springboot旅游推介平台---附源码24175
java·spring boot·python·spring cloud·eclipse·django·idea