LanceDB#

pip install "needtoknow[lancedb]"

LanceDB is an embedded vector database: it runs inside your process, on a local directory or on object storage, with no server to operate. The backend runs LanceDB’s vector search with the permission filter applied before ranking (LanceDB’s pre-filtering), so the k nearest chunks the subject may read come back however few of the table’s chunks that is. CI runs needtoknow’s conformance kit against LanceDB 0.40.

import lancedb

from needtoknow.backends.lancedb import LanceDBBackend, create_indexes, create_table

db = lancedb.connect("data/chunks")  # or an object storage URI
table = create_table(db, "chunks", dim=768)
backend = LanceDBBackend(table, embed=embed)  # embed: str -> list[float]
backend.add(documents)
create_indexes(table)  # scalar indexes on the permission terms and ids, once data is loaded

Each chunk is a row with the columns id, needtoknow_key (the id, hex-encoded), content, metadata (JSON), needtoknow_terms and vector (float32). add upserts by id, and a write is searchable as soon as it returns; when one call adds the same id twice, the last version wins. Scores are cosine similarity, 1 minus LanceDB’s cosine distance. Without an ANN index, LanceDB searches every row exactly; add one as for any LanceDB table, and tune it with nprobes and refine_factor. On a million chunks, LanceDB’s default index (IVF_PQ, no refinement) found about half of the true top 10 at every share readable (Benchmarks); set refine_factor to re-rank with the full vectors before relying on it. Async calls run in a thread. Without embed, write with add(documents, embeddings=...) and search with Query(vector=...).

How permissions are indexed#

The backend uses the same type-tagged terms as Elasticsearch, OpenSearch and Redis, so 1 and "1" stay apart and booleans, floats, nested lists and objects never match. LanceDB filters are SQL, so each term is stored hex-encoded in a list column, and a filter becomes array_has_any over hex strings:

from needtoknow import In
from needtoknow.backends.lancedb import filter_to_sql

filter_to_sql(In("allowed_groups", ["hr"]))
# "array_has_any(needtoknow_terms, ['616c6c6f7765645f67726f7570731f733a6872'])"

A hex string holds only 0-9 and a-f, so no group name or user id is ever part of the SQL; CI checks that with quotes and SQL fragments as group names. Deletes match the hex-encoded id column for the same reason. And and Or become AND and OR, and an In with no values matches nothing without a query. create_indexes adds a LABEL_LIST index on the terms, the index type LanceDB uses for array_has_any; rows added later are found before they are indexed. The metadata is stored as JSON and returned exactly as written, so the retriever’s re-check sees the true permissions.

Rows written by other tools#

A row with no needtoknow_terms is invisible to a pre-filtered search, though post-filtering still finds it and checks its metadata. Fill the column in your ingestion pipeline:

from needtoknow.backends.lancedb import permission_terms

row["needtoknow_terms"] = permission_terms(metadata)

A row whose metadata is not a JSON object carries no permissions, so the re-check denies it.