File size: 8,538 Bytes
de262f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1499b8f
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# **Entity-Relationship Diagram (ERD) & Table Definitions**

## **1. Entity-Relationship Diagram (Logical)**

```mermaid
erDiagram
    USER ||--o{ USER_ROLE : "assigned"
    ROLE ||--o{ USER_ROLE : "assigned to"
    ROLE ||--o{ ROLE_DOCUMENT_ACCESS : "has access to"
    DOCUMENT ||--o{ ROLE_DOCUMENT_ACCESS : "is accessed by"
    DOCUMENT ||--o{ CHUNK : "contains"
    USER ||--o{ QUERY_LOG : "submits"
    QUERY_LOG ||--o| EVALUATION_RESULT : "evaluated by"

    USER {
        uuid id PK
        string email
        string password_hash
        string display_name
        boolean is_active
        timestamp created_at
        timestamp last_login
    }
    ROLE {
        uuid id PK
        string name "e.g., 'researcher', 'guest'"
    }
    USER_ROLE {
        uuid user_id FK
        uuid role_id FK
    }
    DOCUMENT {
        uuid id PK
        string arxiv_id "UNIQUE, from dataset"
        string title
        string source_url "Link to ArXiv"
        string full_citation
        jsonb metadata
        timestamp created_at
    }
    CHUNK {
        uuid id PK
        uuid document_id FK
        text content
        vector embedding "vector(384)"
        int chunk_index
        jsonb metadata "page/section refs"
    }
    ROLE_DOCUMENT_ACCESS {
        uuid role_id FK
        uuid document_id FK
    }
    QUERY_LOG {
        uuid id PK
        uuid user_id FK
        text query_text
        text response_text
        string model_provider
        int agent_steps
        timestamp created_at
    }
    EVALUATION_RESULT {
        uuid id PK
        uuid query_log_id FK
        float relevance_score
        float groundedness_score
        float answer_relevance_score
        string trulens_record_id "Cross-ref to TruLens DB"
        timestamp evaluated_at
    }
```

## **2. Table Definitions (SQL)**

### **2.1 Core Tables**

```sql
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Users Table
CREATE TABLE users (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email         TEXT UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    display_name  TEXT,
    is_active     BOOLEAN NOT NULL DEFAULT TRUE,
    created_at    TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_login    TIMESTAMP WITH TIME ZONE
);

-- Roles Table
CREATE TABLE roles (
    id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT UNIQUE NOT NULL  -- e.g., 'researcher', 'guest'
);

-- User-Role Junction Table (role-level RBAC only β€” no per-user document grants)
CREATE TABLE user_roles (
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

-- Documents Table
CREATE TABLE documents (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    arxiv_id      TEXT UNIQUE NOT NULL,
    title         TEXT,
    source_url    TEXT,        -- Direct link for user verification
    full_citation TEXT,        -- Bibliographic reference
    metadata      JSONB,
    created_at    TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Chunks Table (each chunk belongs to exactly one document)
CREATE TABLE chunks (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    content     TEXT NOT NULL,
    embedding   VECTOR(384),
    chunk_index INT NOT NULL,
    metadata    JSONB          -- Stores page numbers, section headers, etc.
);

-- Role-Document Access Control
CREATE TABLE role_document_access (
    role_id     UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, document_id)
);
```

### **2.2 Observability Tables**

```sql
-- Query Log: persists every agentic query and its final response
CREATE TABLE query_logs (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id        UUID REFERENCES users(id) ON DELETE SET NULL,
    query_text     TEXT NOT NULL,
    response_text  TEXT,
    model_provider TEXT,   -- 'openai' | 'azure_openai' | 'gemini'
    agent_steps    INT,    -- SmolAgents steps taken (max 5)
    created_at     TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Evaluation Results: RAG Triad scores, cross-referenced to TruLens
CREATE TABLE evaluation_results (
    id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    query_log_id          UUID NOT NULL REFERENCES query_logs(id) ON DELETE CASCADE,
    relevance_score       NUMERIC(4,3),  -- Query vs. Reranked Context  (0–1)
    groundedness_score    NUMERIC(4,3),  -- Answer vs. Reranked Context (0–1)
    answer_relevance_score NUMERIC(4,3), -- Query vs. Final Answer      (0–1)
    trulens_record_id     TEXT,          -- Maps to TruLens Record.record_id
    evaluated_at          TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```

## **3. Indexing Recommendations**

### **3.1 Vector Index β€” HNSW (384-dim, Cosine Distance)**

HNSW is preferred over IVFFlat: better recall at query time, no need to pre-specify cluster counts, and no index rebuild required after inserts. Parameters are tuned for 384-dimensional sentence-transformer embeddings.

```sql
-- Cosine distance is standard for sentence-transformer embeddings
CREATE INDEX idx_chunks_embedding_hnsw
ON chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Tune recall vs. latency at query time (default: 40):
-- SET hnsw.ef_search = 100;
```

| Parameter | Value | Rationale |
|---|---|---|
| `m` | 16 | Balanced graph connectivity; increase to 32 for higher recall at cost of memory |
| `ef_construction` | 64 | Build-time quality; increase to 128 for finer index at cost of build time |
| `ef_search` | 40–100 | Set per-session; 40 for <50 ms, 100 for maximum recall |
| Distance op | `vector_cosine_ops` | Standard for normalized sentence-transformer output |

### **3.2 Access Control & Foreign Key Indexes**

```sql
-- Speed up RBAC JOIN: "WHERE document_id IN (authorized_list)"
CREATE INDEX idx_chunks_document_id ON chunks(document_id);

-- Speed up role resolution for a given user
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);

-- Speed up document permission checks
CREATE INDEX idx_role_doc_access_doc_id ON role_document_access(document_id);
```

### **3.3 Observability Indexes**

```sql
-- Time-ordered query history per user (pagination, monitoring dashboards)
CREATE INDEX idx_query_logs_user_id_created ON query_logs(user_id, created_at DESC);

-- Evaluation lookup by query
CREATE INDEX idx_eval_results_query_log_id ON evaluation_results(query_log_id);

-- TruLens cross-reference (partial β€” only rows that have been evaluated)
CREATE INDEX idx_eval_results_trulens_id
ON evaluation_results(trulens_record_id)
WHERE trulens_record_id IS NOT NULL;
```

## **4. Example Access-Aware Query with Citations**

When the Agent performs a search, it retrieves `source_url`, `title`, and `full_citation` so it can cite its sources in the final response.

```sql
SELECT
    c.content,
    d.title,
    d.source_url,
    d.full_citation,
    c.metadata->>'page'                      AS page_number,
    c.embedding <=> '[query_vector]'::vector AS distance
FROM chunks c
JOIN documents d             ON c.document_id  = d.id
JOIN role_document_access rda ON d.id           = rda.document_id
JOIN user_roles ur            ON rda.role_id    = ur.role_id
WHERE ur.user_id = 'current_user_id'
ORDER BY distance
LIMIT 20;
```

## **5. TruLens Integration Strategy**

TruLens maintains its own internal schema for detailed trace data. The `evaluation_results` table here serves as a **lightweight application-side mirror** for:

* Querying aggregate RAG Triad scores without launching the TruLens dashboard.
* Joining scores directly to your own `users` and `query_logs` data for reporting.
* Setting up alerting when scores fall below SLA thresholds (relevance > 0.85, groundedness > 0.9).

The `trulens_record_id` column maps to TruLens's `Record.record_id`, enabling a full round-trip lookup when deeper trace or span inspection is needed.

Application evaluation code waits for feedback completion (`TRULENS_FEEDBACK_TIMEOUT`, `retrieve_feedback_results` in `backend/app/evaluation/trulens_eval.py`) before writing `evaluation_results`. The **context** embedded in the TruLens replay for Context Relevance and Groundedness is aligned with the **last `pgvector_retriever` formatted output** when available (`reranked_context`), not only LLM JSON.