|
3 | 3 | from datetime import date, datetime |
4 | 4 | from typing import TYPE_CHECKING, List, Optional |
5 | 5 |
|
6 | | -from sqlalchemy import JSON, BigInteger, Date, DateTime, String, Text |
| 6 | +from pgvector.sqlalchemy import Vector |
7 | 7 | from sqlalchemy import Enum as SqlEnum |
| 8 | +from sqlalchemy import ( |
| 9 | + JSON, |
| 10 | + BigInteger, |
| 11 | + Date, |
| 12 | + DateTime, |
| 13 | + String, |
| 14 | + Text, |
| 15 | + Index, |
| 16 | +) |
8 | 17 | from sqlalchemy.orm import Mapped, mapped_column, relationship |
9 | 18 | from sqlalchemy.sql import func |
10 | 19 |
|
|
13 | 22 |
|
14 | 23 |
|
15 | 24 | class Paper(Base): |
16 | | - """Database representation of a scholarly paper.""" |
| 25 | + """Database representation of a scholarly paper with vector embeddings.""" |
17 | 26 |
|
18 | 27 | __tablename__ = "paper" |
19 | 28 |
|
| 29 | + # ------------------ |
| 30 | + # Columns |
| 31 | + # ------------------ |
20 | 32 | paper_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True) |
21 | 33 | doi: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) |
| 34 | + |
22 | 35 | source: Mapped[PaperSource] = mapped_column( |
23 | 36 | SqlEnum(PaperSource, name="paper_source"), nullable=False |
24 | 37 | ) |
| 38 | + |
25 | 39 | paper_type: Mapped[PaperType] = mapped_column( |
26 | 40 | SqlEnum(PaperType, name="paper_type"), |
27 | 41 | nullable=False, |
28 | 42 | default=PaperType.PREPRINT, |
29 | 43 | server_default=PaperType.PREPRINT.value, |
30 | 44 | ) |
| 45 | + |
31 | 46 | title: Mapped[str] = mapped_column(String(512), nullable=False) |
32 | 47 | authors: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) |
33 | 48 | abstract: Mapped[Optional[str]] = mapped_column(Text, nullable=True) |
34 | 49 | published_at: Mapped[Optional[date]] = mapped_column(Date, nullable=True) |
35 | 50 | pdf_url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) |
36 | 51 | url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) |
| 52 | + |
37 | 53 | fetched_at: Mapped[datetime] = mapped_column( |
38 | 54 | DateTime(timezone=True), nullable=False, server_default=func.now() |
39 | 55 | ) |
40 | 56 |
|
| 57 | + # ------------------ |
| 58 | + # Vector Embedding |
| 59 | + # ------------------ |
| 60 | + embedding: Mapped[Optional[List[float]]] = mapped_column( |
| 61 | + Vector(768), nullable=True |
| 62 | + ) |
| 63 | + |
| 64 | + # ------------------ |
| 65 | + # Relationships |
| 66 | + # ------------------ |
41 | 67 | project_links: Mapped[List["ProjectPaper"]] = relationship( |
42 | 68 | "ProjectPaper", back_populates="paper", cascade="all, delete-orphan" |
43 | 69 | ) |
| 70 | + |
44 | 71 | projects: Mapped[List["Project"]] = relationship( |
45 | 72 | "Project", secondary="project_paper", back_populates="papers" |
46 | 73 | ) |
47 | 74 |
|
| 75 | + # ------------------ |
| 76 | + # Indexes (HNSW) |
| 77 | + # ------------------ |
| 78 | + __table_args__ = ( |
| 79 | + Index( |
| 80 | + "ix_paper_embedding_hnsw", |
| 81 | + "embedding", |
| 82 | + postgresql_using="hnsw", |
| 83 | + postgresql_with={"m": 16, "ef_search": 40, "lists": 100}, |
| 84 | + ), |
| 85 | + ) |
| 86 | + |
48 | 87 |
|
49 | 88 | if TYPE_CHECKING: |
50 | 89 | from .project import Project |
|
0 commit comments