Spaces:
Running
Running
| from __future__ import annotations | |
| import argparse | |
| from array import array | |
| import hashlib | |
| import json | |
| import math | |
| from pathlib import Path | |
| import re | |
| import sqlite3 | |
| from typing import Iterable, List, Sequence, Tuple | |
| WORD_RE = re.compile(r"[A-Za-z0-9_]+") | |
| def connect(db_path: str) -> sqlite3.Connection: | |
| Path(db_path).parent.mkdir(parents=True, exist_ok=True) | |
| con = sqlite3.connect(db_path) | |
| con.execute("PRAGMA journal_mode=WAL") | |
| con.execute("PRAGMA synchronous=NORMAL") | |
| return con | |
| def init_db(con: sqlite3.Connection, dim: int = 384) -> None: | |
| con.execute( | |
| """ | |
| CREATE TABLE IF NOT EXISTS docs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| title TEXT, | |
| source TEXT, | |
| text TEXT NOT NULL, | |
| meta_json TEXT DEFAULT '{}', | |
| vector BLOB, | |
| dim INTEGER NOT NULL | |
| ) | |
| """ | |
| ) | |
| try: | |
| con.execute("CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(title, text, source, content='docs', content_rowid='id')") | |
| except sqlite3.OperationalError as exc: | |
| raise SystemExit("Your SQLite build needs FTS5 enabled for this RAG store.") from exc | |
| con.execute("CREATE INDEX IF NOT EXISTS idx_docs_source ON docs(source)") | |
| con.execute("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)") | |
| con.execute("INSERT OR REPLACE INTO meta(key, value) VALUES('vector_dim', ?)", (str(dim),)) | |
| con.commit() | |
| def chunk_text(text: str, max_chars: int = 1200, overlap: int = 120) -> Iterable[str]: | |
| text = re.sub(r"\s+", " ", text).strip() | |
| if not text: | |
| return | |
| start = 0 | |
| while start < len(text): | |
| end = min(len(text), start + max_chars) | |
| yield text[start:end] | |
| if end == len(text): | |
| break | |
| start = max(0, end - overlap) | |
| def hashed_embedding(text: str, dim: int = 384) -> array: | |
| """Deterministic no-external-model lexical embedding. | |
| This is not a neural embedding. It is a bootstrap retriever that can later be replaced by an | |
| embedding head trained inside Ares. | |
| """ | |
| vec = array("f", [0.0]) * dim | |
| words = [w.lower() for w in WORD_RE.findall(text)] | |
| features: List[str] = [] | |
| features.extend(words) | |
| features.extend([" ".join(words[i : i + 2]) for i in range(max(0, len(words) - 1))]) | |
| features.extend([" ".join(words[i : i + 3]) for i in range(max(0, len(words) - 2))]) | |
| for feat in features: | |
| h = hashlib.blake2b(feat.encode("utf-8"), digest_size=8).digest() | |
| bucket = int.from_bytes(h[:4], "little") % dim | |
| sign = 1.0 if (h[4] & 1) else -1.0 | |
| vec[bucket] += sign | |
| norm = math.sqrt(sum(x * x for x in vec)) or 1.0 | |
| for i, x in enumerate(vec): | |
| vec[i] = x / norm | |
| return vec | |
| def vector_to_blob(vec: array) -> bytes: | |
| return vec.tobytes() | |
| def blob_to_vector(blob: bytes) -> array: | |
| vec = array("f") | |
| vec.frombytes(blob) | |
| return vec | |
| def cosine(a: array, b: array) -> float: | |
| return float(sum(x * y for x, y in zip(a, b))) | |
| def add_doc(con: sqlite3.Connection, title: str, text: str, source: str = "", meta=None, dim: int = 384) -> int: | |
| meta = meta or {} | |
| vec = hashed_embedding(title + "\n" + text, dim=dim) | |
| cur = con.execute( | |
| "INSERT INTO docs(title, source, text, meta_json, vector, dim) VALUES (?, ?, ?, ?, ?, ?)", | |
| (title, source, text, json.dumps(meta), vector_to_blob(vec), dim), | |
| ) | |
| doc_id = int(cur.lastrowid) | |
| con.execute("INSERT INTO docs_fts(rowid, title, text, source) VALUES (?, ?, ?, ?)", (doc_id, title, text, source)) | |
| return doc_id | |
| def ingest_paths(con: sqlite3.Connection, paths: Sequence[str], dim: int = 384) -> int: | |
| count = 0 | |
| files: List[Path] = [] | |
| for item in paths: | |
| p = Path(item) | |
| if p.is_dir(): | |
| files.extend([x for x in sorted(p.rglob("*")) if x.is_file() and x.suffix.lower() in {".txt", ".md"}]) | |
| elif p.is_file(): | |
| files.append(p) | |
| else: | |
| raise FileNotFoundError(item) | |
| for p in files: | |
| text = p.read_text(encoding="utf-8", errors="ignore") | |
| for i, chunk in enumerate(chunk_text(text)): | |
| add_doc(con, title=f"{p.name} chunk {i}", text=chunk, source=str(p), meta={"chunk": i}, dim=dim) | |
| count += 1 | |
| con.commit() | |
| return count | |
| def search(con: sqlite3.Connection, query: str, limit: int = 5, dim: int = 384) -> List[Tuple[int, str, str, float]]: | |
| qvec = hashed_embedding(query, dim=dim) | |
| # FTS candidate pool, then vector rerank. If FTS syntax rejects query, use a broad lexical fallback. | |
| try: | |
| rows = con.execute( | |
| """ | |
| SELECT d.id, d.title, d.text, d.vector, bm25(docs_fts) AS bm25 | |
| FROM docs_fts | |
| JOIN docs d ON docs_fts.rowid = d.id | |
| WHERE docs_fts MATCH ? | |
| ORDER BY bm25 | |
| LIMIT ? | |
| """, | |
| (query, max(25, limit * 5)), | |
| ).fetchall() | |
| except sqlite3.OperationalError: | |
| terms = [t.lower() for t in WORD_RE.findall(query)] | |
| like = "%" + "%".join(terms[:4]) + "%" if terms else "%" | |
| rows = con.execute( | |
| "SELECT id, title, text, vector, 0.0 FROM docs WHERE lower(text) LIKE ? LIMIT ?", | |
| (like, max(25, limit * 5)), | |
| ).fetchall() | |
| if not rows: | |
| rows = con.execute( | |
| "SELECT id, title, text, vector, 0.0 FROM docs LIMIT ?", | |
| (max(25, limit * 5),), | |
| ).fetchall() | |
| scored = [] | |
| for doc_id, title, text, blob, bm25_score in rows: | |
| score = cosine(qvec, blob_to_vector(blob)) - 0.01 * float(bm25_score) | |
| scored.append((int(doc_id), str(title), str(text), float(score))) | |
| scored.sort(key=lambda x: x[3], reverse=True) | |
| return scored[:limit] | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Ares SQLite/FTS5 RAG bootstrap store.") | |
| sub = parser.add_subparsers(dest="cmd", required=True) | |
| p_init = sub.add_parser("init") | |
| p_init.add_argument("--db", required=True) | |
| p_init.add_argument("--dim", type=int, default=384) | |
| p_ingest = sub.add_parser("ingest") | |
| p_ingest.add_argument("--db", required=True) | |
| p_ingest.add_argument("--input", nargs="+", required=True) | |
| p_ingest.add_argument("--dim", type=int, default=384) | |
| p_search = sub.add_parser("search") | |
| p_search.add_argument("--db", required=True) | |
| p_search.add_argument("--query", required=True) | |
| p_search.add_argument("--limit", type=int, default=5) | |
| p_search.add_argument("--dim", type=int, default=384) | |
| args = parser.parse_args() | |
| con = connect(args.db) | |
| if args.cmd == "init": | |
| init_db(con, dim=args.dim) | |
| print(f"Initialized {args.db}") | |
| elif args.cmd == "ingest": | |
| init_db(con, dim=args.dim) | |
| n = ingest_paths(con, args.input, dim=args.dim) | |
| print(f"Ingested {n} chunks into {args.db}") | |
| elif args.cmd == "search": | |
| results = search(con, args.query, limit=args.limit, dim=args.dim) | |
| for doc_id, title, text, score in results: | |
| print(json.dumps({"id": doc_id, "title": title, "score": score, "text": text[:500]}, ensure_ascii=False)) | |
| if __name__ == "__main__": | |
| main() | |