Import source: axon/services/index
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from axon.config import settings
|
||||
from axon.models import IndexDocument, IngestDocument, SearchResult
|
||||
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
|
||||
def _db_path() -> Path:
|
||||
return Path(settings.index_db_path)
|
||||
|
||||
|
||||
def _connect() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(_db_path())
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
async def init_index() -> None:
|
||||
def _init() -> None:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
url TEXT,
|
||||
metadata TEXT NOT NULL DEFAULT '{}',
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
|
||||
title,
|
||||
content,
|
||||
content='documents',
|
||||
content_rowid='rowid'
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN
|
||||
INSERT INTO documents_fts(rowid, title, content)
|
||||
VALUES (new.rowid, new.title, new.content);
|
||||
END
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN
|
||||
INSERT INTO documents_fts(documents_fts, rowid, title, content)
|
||||
VALUES ('delete', old.rowid, old.title, old.content);
|
||||
END
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN
|
||||
INSERT INTO documents_fts(documents_fts, rowid, title, content)
|
||||
VALUES ('delete', old.rowid, old.title, old.content);
|
||||
INSERT INTO documents_fts(rowid, title, content)
|
||||
VALUES (new.rowid, new.title, new.content);
|
||||
END
|
||||
"""
|
||||
)
|
||||
|
||||
async with _lock:
|
||||
await asyncio.to_thread(_init)
|
||||
|
||||
|
||||
async def ingest_documents(documents: list[IngestDocument]) -> int:
|
||||
await init_index()
|
||||
|
||||
def _ingest() -> int:
|
||||
with _connect() as conn:
|
||||
for doc in documents:
|
||||
doc_id = doc.id or str(uuid.uuid4())
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO documents(id, title, content, url, metadata, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title=excluded.title,
|
||||
content=excluded.content,
|
||||
url=excluded.url,
|
||||
metadata=excluded.metadata,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
""",
|
||||
(
|
||||
doc_id,
|
||||
doc.title,
|
||||
doc.content,
|
||||
doc.url,
|
||||
json.dumps(doc.metadata, sort_keys=True),
|
||||
),
|
||||
)
|
||||
return len(documents)
|
||||
|
||||
async with _lock:
|
||||
return await asyncio.to_thread(_ingest)
|
||||
|
||||
|
||||
async def search_index(query: str, max_results: int = 10) -> list[IndexDocument]:
|
||||
await init_index()
|
||||
|
||||
def _search() -> list[IndexDocument]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
documents.id,
|
||||
documents.title,
|
||||
snippet(documents_fts, 1, '', '', '...', 40) AS content,
|
||||
documents.url,
|
||||
bm25(documents_fts) AS rank
|
||||
FROM documents_fts
|
||||
JOIN documents ON documents_fts.rowid = documents.rowid
|
||||
WHERE documents_fts MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT ?
|
||||
""",
|
||||
(query, max_results),
|
||||
).fetchall()
|
||||
results = []
|
||||
for row in rows:
|
||||
rank = float(row["rank"])
|
||||
score = max(0.0, min(1.0, 1.0 / (1.0 + abs(rank))))
|
||||
results.append(
|
||||
IndexDocument(
|
||||
id=row["id"],
|
||||
title=row["title"],
|
||||
content=row["content"] or "",
|
||||
url=row["url"],
|
||||
score=round(score, 6),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
async with _lock:
|
||||
return await asyncio.to_thread(_search)
|
||||
|
||||
|
||||
async def search_index_as_results(query: str, max_results: int = 5) -> list[SearchResult]:
|
||||
docs = await search_index(query, max_results=max_results)
|
||||
return [
|
||||
SearchResult(
|
||||
title=doc.title,
|
||||
url=doc.url or f"private://{doc.id}",
|
||||
content=doc.content,
|
||||
score=doc.score,
|
||||
)
|
||||
for doc in docs
|
||||
]
|
||||
Reference in New Issue
Block a user