Extending needtoknow#
Any vector store and any permission system can be plugged in. Both sides are small protocols, so there is nothing to subclass.
A backend#
A backend is any object with a search method and a supports_filters attribute:
from needtoknow import Filter, Hit
class MyBackend:
supports_filters = True # False if search() ignores `filter`
def search(self, query: str, k: int, filter: Filter | None = None) -> list[Hit]:
"""Return at most k hits, best first, with each chunk's stored metadata."""
With supports_filters = True, translate the filter into your store’s native filter. The rules
that make translations agree are in Concepts: a missing field never
matches, only strings and integers match, list fields match when they share an element, and
In(field, []) matches nothing. Return the metadata exactly as stored, because the re-check
decides on it.
Add async def asearch(...) with the same signature for a native async path; otherwise
aretrieve runs search in a thread.
Optionally, give it set_metadata(metadata: Mapping[str, Mapping[str, Any]]) to replace stored
chunks’ metadata without re-embedding them (Changing a document’s permissions):
replace the whole metadata, ignore ids that are not stored, and return once searches see the
change. The conformance kit checks it when it is there.
query is whatever the caller passed to retrieve. String queries arrive as strings; a caller
with its own vector passes a Query, and Query.of(query).embedded(self.embed) gives the vector
to search with either way. See Your own vectors.
Test it with the conformance kit#
needtoknow.testing checks a backend or an authorizer the way the built-in ones are checked in
needtoknow’s CI. It generates documents, permission metadata and nested filters from a seed (with
scalar and list fields, mixed value types and missing fields: the cases where filter translations
tend to go wrong) and compares your implementation with the reference semantics. It needs no extra
packages, no model and no network.
from needtoknow.testing import HashingEmbedder, check_authorizer, check_backend
def make_backend(documents):
backend = MyStoreBackend(client, embed=HashingEmbedder(), collection=new_collection())
backend.add(documents)
return backend
def test_my_backend_conforms():
check_backend(make_backend)
def test_my_authorizer_conforms():
check_authorizer(MyAuthorizer(...), subjects, hits) # subjects and hits from your test data
check_backend checks that a search returns exactly the documents matches selects, at most k,
best first, with text and metadata exactly as stored (the retriever re-checks pre-filtered hits
against that metadata), that asearch agrees with search, and that retrieval through
PermissionedRetriever never returns a chunk the subject may not read. For an approximate index
that may miss matches, pass exact=False: a result may then be short, but never wrong.
check_authorizer checks that check returns one boolean per hit, that a hit’s answer does not
depend on the rest of the batch, and that check agrees with the filter from filter_for. If the
two disagree, pre-filtered and post-filtered results differ.
A failure raises ConformanceError with the seed, the example number, the documents, the filter
and what came back. For shrinking to a minimal failing example, the same properties can be written
with Hypothesis, as needtoknow’s own
tests/test_backends.py
does.
Contributing an adapter#
New backends and authorizers are welcome. An adapter goes in its own module behind an optional
extra, passes check_backend or check_authorizer against the real service in CI, and fails
closed on every error. See the
contributing guide.