Skip to content

Commit 6ceed65

Browse files
authored
Merge pull request #85 from NetherlandsForensicInstitute/feature/add-postgresql-database-interface
Add PostgreSQL database interface
2 parents 1eb34a9 + 0f6efb8 commit 6ceed65

6 files changed

Lines changed: 476 additions & 49 deletions

File tree

.github/workflows/citatio.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,34 @@ jobs:
2727
test:
2828
runs-on: ubuntu-latest
2929
needs: check
30-
strategy:
31-
matrix:
32-
python-version: ['3.13']
3330
defaults:
3431
run:
3532
working-directory: citatio/
33+
services:
34+
pgvector:
35+
image: pgvector/pgvector:pg18
36+
ports:
37+
- 5432:5432
38+
env:
39+
POSTGRES_PASSWORD: tee4ieCh1jeerahkie3u
40+
options: >-
41+
--health-cmd pg_isready
42+
--health-interval 10s
43+
--health-timeout 5s
44+
--health-retries 5
3645
steps:
3746
- uses: actions/checkout@v6
3847
with:
3948
fetch-depth: 20
4049
- uses: pdm-project/setup-pdm@v4
4150
with:
42-
python-version: ${{ matrix.python-version }}
51+
python-version: '3.13'
4352
cache: true
4453
cache-dependency-path: 'citatio/pdm.lock'
4554
- run: pdm install --group test
4655
- run: pdm run test
56+
- run: pdm run integration-test
57+
env:
58+
POSTGRES_HOST: localhost
59+
POSTGRES_PORT: 5432
60+
POSTGRES_PASSWORD: tee4ieCh1jeerahkie3u

citatio/citatio/db.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import sqlite3
22
from importlib import resources
33

4+
import asyncpg
45
import numpy as np
56
import sqlite_vec
7+
from pgvector.asyncpg import register_vector
68

79

810
class Database:
@@ -106,11 +108,65 @@ def search_function(self, embedding, top_n=25):
106108

107109

108110
class PostgreSQLDatabase:
109-
def __init__(self):
110-
raise TypeError
111+
def __init__(self, connection):
112+
self.connection = connection
111113

112-
def add_function(self, name, cfg, embedding, binary_name, binary_sha256, model_identifier=None):
113-
raise NotImplementedError
114+
@classmethod
115+
async def connect(cls, **kwargs):
116+
connection = await asyncpg.connect(**kwargs)
117+
await connection.execute('CREATE EXTENSION IF NOT EXISTS vector')
118+
await register_vector(connection)
119+
await connection.execute(resources.read_text('citatio', 'schema-postgresql.sql'))
120+
return cls(connection)
114121

115-
def search_function(self, embedding, top_n=25):
116-
raise NotImplementedError
122+
async def _insert_or_get_function(self, cfg, embedding):
123+
cfg = str(cfg)
124+
try:
125+
return await self.connection.fetchval(
126+
'INSERT INTO functions (cfg, embedding) VALUES ($1, $2) RETURNING id',
127+
cfg,
128+
embedding,
129+
)
130+
except asyncpg.IntegrityConstraintViolationError:
131+
return await self.connection.fetchval('SELECT id FROM functions WHERE cfg = $1', cfg)
132+
133+
async def _insert_label(self, function_id, name, binary_name, binary_sha256):
134+
await self.connection.execute(
135+
'INSERT INTO labels (function_id, label, binary_name, binary_sha256) VALUES ($1, $2, $3, $4)',
136+
function_id,
137+
name,
138+
binary_name,
139+
binary_sha256,
140+
)
141+
142+
async def add_function(self, name, cfg, embedding, binary_name, binary_sha256, model_identifier=None):
143+
function_id = await self._insert_or_get_function(cfg, embedding)
144+
await self._insert_label(function_id, name, binary_name, binary_sha256)
145+
return function_id
146+
147+
async def _search_near(self, embedding, top_n):
148+
return await self.connection.fetch(
149+
"""
150+
SELECT label, (2 - (embedding <-> $1)) / 2 AS similarity, binary_name, binary_sha256
151+
FROM labels
152+
JOIN functions ON labels.function_id = functions.id
153+
ORDER BY similarity DESC
154+
LIMIT $2
155+
""",
156+
embedding,
157+
top_n,
158+
)
159+
160+
async def search_function(self, embedding, top_n=25):
161+
results = await self._search_near(embedding, top_n)
162+
163+
return [
164+
dict(
165+
zip(
166+
['function', 'similarity', 'binary_name', 'binary_sha256'],
167+
result,
168+
strict=True,
169+
)
170+
)
171+
for result in results
172+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
CREATE TABLE IF NOT EXISTS functions (
2+
id SERIAL PRIMARY KEY,
3+
-- UNIQUE constraint allows using an error trigger as a 'cfg is already known' signal
4+
cfg TEXT NOT NULL UNIQUE,
5+
embedding VECTOR(768)
6+
);
7+
8+
CREATE INDEX IF NOT EXISTS embeddings_cosine ON functions USING hnsw (embedding vector_cosine_ops);
9+
10+
CREATE TABLE IF NOT EXISTS labels (
11+
id SERIAL PRIMARY KEY,
12+
function_id INTEGER NOT NULL,
13+
label TEXT NOT NULL,
14+
-- (binary_name, binary_sha256) to identify the source of this label
15+
binary_name TEXT NOT NULL,
16+
binary_sha256 TEXT NOT NULL,
17+
18+
FOREIGN KEY (function_id) REFERENCES functions(id)
19+
);

0 commit comments

Comments
 (0)