用Python+向量索引打造学术论文语义搜索引擎(CocoIndex实战教程)研究者和技术社区都面临一个痛点:大量PDF论文散落在硬盘,想按作者、领域甚至语义检索变得极其困难。关键字搜索已经不能满足真实需求,语义搜索+元数据检索才是新时代的打开方式。
本文实战分享如何用CocoIndex,一套开源AI数据管道,只需不到200行Python代码,把本地PDF库变身为支持语义查询、作者检索、SQL联表、自动增量刷新的一站式学术搜索系统。
为什么不要只把PDF强行embed?
主流RAG检索都是:按页或分块插入PDF到向量数据库,只能模糊召回。 但实际问题包括:
- 没法按作者/年份/页数精确筛选
- 「展示Geoffrey Hinton 2019之后的全部论文」根本无法一条SQL解决
- 向量模型常常embed了大量无关内容,附录/ author info 占大头
- 文本检索和“top-K向量”混合逻辑极其复杂
CocoIndex的方案是:
- 保持元数据(标题、作者、摘要、页数)结构化和独立
- 嵌入title和摘要分块(chunk)实现语义召回
- 所有实体直接存储在PostgreSQL+PGVector,SQL随意查
技术架构/全流程拆解
- 监控papers目录,自动获取新PDF(支持刷新)
- 每篇PDF:只提取第一页,保存页数
- 用Marker(或Docling)把第一页转Markdown
- LLM结构化抽取title/author/abstract(直接dataclass)
- 摘要分块并embed,每个chunk用all-MiniLM-L6-v2模型算向量
- 三张表存Postgres:
- paper_metadata
- author_papers
- metadata_embeddings
关键代码与表结构
@cocoindex.flow_def(name="PaperMetadata")
def paper_metadata_flow(flow_builder, data_scope):
data_scope["documents"] = flow_builder.add_source(
cocoindex.sources.LocalFile(path="papers", binary=True),
refresh_interval=datetime.timedelta(seconds=10)
)
@dataclasses.dataclass
class PaperBasicInfo:
num_pages: int
first_page: bytes
@cocoindex.op.function()
def extract_basic_info(content: bytes) -> PaperBasicInfo:
reader = PdfReader(io.BytesIO(content))
writer = PdfWriter()
writer.add_page(reader.pages[0])
output = io.BytesIO()
writer.write(output)
return PaperBasicInfo(
num_pages=len(reader.pages),
first_page=output.getvalue()
)
LLM与嵌入
@dataclasses.dataclass
class Author:
name: str
@dataclasses.dataclass
class PaperMetadata:
title: str
authors: list[Author]
abstract: str
with data_scope["documents"].row() as doc:
doc["first_page_md"] = doc["basic_info"]["first_page"].transform(pdf_to_markdown)
doc["metadata"] = doc["first_page_md"].transform(
cocoindex.functions.ExtractByLlm(
llm_spec=cocoindex.LlmSpec(
api_type=cocoindex.LlmApiType.OPENAI, model="gpt-4o"
),
output_type=PaperMetadata,
instruction="请抽取标题、作者和摘要"
)
)
doc["title_embedding"] = doc["metadata"]["title"].transform(
cocoindex.functions.SentenceTransformerEmbed(
model="sentence-transformers/all-MiniLM-L6-v2"
)
)
doc["abstract_chunks"] = doc["metadata"]["abstract"].transform(
cocoindex.functions.SplitRecursively(
chunk_size=500,
chunk_overlap=150
)
)
with doc["abstract_chunks"].row() as chunk:
chunk["embedding"] = chunk["text"].transform(
cocoindex.functions.SentenceTransformerEmbed(
model="sentence-transformers/all-MiniLM-L6-v2"
)
)
Postgres表结构与典型查询
paper_metadata.export(
"paper_metadata",
cocoindex.targets.Postgres(),
primary_key_fields=["filename"]
)
author_papers.export(
"author_papers",
cocoindex.targets.Postgres(),
primary_key_fields=["author_name", "filename"]
)
metadata_embeddings.export(
"metadata_embeddings",
cocoindex.targets.Postgres(),
vector_indexes=[cocoindex.VectorIndexDef(field_name="embedding")]
)
查询案例(直接SQL)
语义检索:
SELECT filename, text FROM metadata_embeddings ORDER BY embedding <=> '[query_vector]' LIMIT 10;
作者检索:
SELECT filename FROM author_papers WHERE author_name = 'Geoffrey Hinton';
混合筛选:
SELECT p.title, p.abstract FROM paper_metadata p JOIN author_papers a ON p.filename = a.filename WHERE a.author_name = 'Yann LeCun' AND p.num_pages < 20;
为什么CocoIndex一定要入手?
- Python全流程,开源易用,数据安全本地可控
- 支持增量处理,批量PDF随时插入自动索引
- 类型安全,结构化数据抽取简洁可靠
- 支持多模型、可替换AI算子/数据库
- 质量高、社区活跃,GitHub已超千星