以编程方式使用文档

SpiceDBRetriever 是一个 LangChain BaseRetriever 包装任意现有检索器并添加 SpiceDB 授权功能。它遵循后置过滤授权模式:先基于语义搜索检索文档,然后按用户权限进行过滤。

安装

pip install langchain-spicedb

设置

环境设置

# SpiceDB connection details
os.environ["SPICEDB_ENDPOINT"] = "localhost:50051"
os.environ["SPICEDB_TOKEN"] = "sometoken"

初始化

from langchain_spicedb import SpiceDBRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Create base retriever (any vector store works)
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
base_retriever = vectorstore.as_retriever()

# Wrap with SpiceDB authorization
auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    subject_type="user",
    permission="view",
    resource_id_key="article_id",
)

参数

  • base_retriever (BaseRetriever):要包装授权的基础检索器(必需)
  • subject_id (str):要检查权限的用户 ID(必需)
  • spicedb_endpoint (str):SpiceDB 服务器地址(默认:"localhost:50051")
  • spicedb_token (str):SpiceDB 认证的预共享密钥(默认:"sometoken")
  • resource_type (str):SpiceDB 资源类型,如 "document"、"article"(默认:"document")
  • subject_type (str):SpiceDB 主体类型,如 "user"(默认:"user")
  • permission (str):要检查的权限,如 "view"、"edit"(默认:"view")
  • resource_id_key (str):文档元数据中包含资源 ID 的键(默认:"resource_id")
  • fail_open (bool):如果为 True,则错误时允许访问;如果为 False,则错误时拒绝访问(默认:False)
  • use_tls (bool):是否对 SpiceDB 连接使用 TLS(默认:False)

用法

基本 RAG 管道

from langchain_spicedb import SpiceDBRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# Setup vector store
documents = [...]  # Your documents with metadata
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
base_retriever = vectorstore.as_retriever()

# Wrap with authorization
auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    subject_type="user",
    permission="view",
    resource_id_key="article_id",
)

# Build RAG chain
prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer based only on the provided context."),
    ("human", "Question: {question}\n\nContext:\n{context}")
])

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

chain = (
    {"context": auth_retriever | format_docs, "question": lambda x: x}
    | prompt
    | ChatOpenAI(model="gpt-4o-mini")
    | StrOutputParser()
)

# Query with authorization
answer = await chain.ainvoke("What is SpiceDB?")
print(answer)

向量存储兼容性

此检索器适用于任何与 LangChain 兼容的向量存储:

FAISS

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    resource_id_key="article_id",
    permission="view",
)

Chroma

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

vectorstore = Chroma.from_documents(documents, OpenAIEmbeddings())
base_retriever = vectorstore.as_retriever()

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="document",
    resource_id_key="doc_id",
    permission="view",
)

Pinecone

from langchain_pinecone import PineconeVectorStore
from langchain_openai import OpenAIEmbeddings

vectorstore = PineconeVectorStore.from_existing_index(
    index_name="my-index",
    embedding=OpenAIEmbeddings()
)
base_retriever = vectorstore.as_retriever()

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    resource_id_key="article_id",
    permission="view",
)

Weaviate

from langchain_weaviate import WeaviateVectorStore
from langchain_openai import OpenAIEmbeddings

vectorstore = WeaviateVectorStore.from_documents(
    documents,
    OpenAIEmbeddings(),
    client=weaviate_client,
    index_name="Article"
)
base_retriever = vectorstore.as_retriever()

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    resource_id_key="article_id",
    permission="view",
)

文档元数据要求

文档 **必须** 在元数据中包含资源 ID:

from langchain_core.documents import Document

# Correct: Document with resource ID
doc = Document(
    page_content="SpiceDB is an open-source authorization system...",
    metadata={
        "article_id": "doc123",  # Must match resource_id_key parameter
        "title": "Introduction to SpiceDB",
        "author": "AuthZed",
    }
)

# The retriever will filter this document based on whether the user
# has permission to view article:doc123 in SpiceDB

授权流程

检索器遵循此流程:

  1. **语义搜索**:基础检索器执行语义搜索并返回前K个文档
  2. **提取资源ID**:从文档元数据中提取资源ID
  3. **批量权限检查**:在单个SpiceDB API调用中检查所有权限
  4. **过滤**:仅返回用户有权查看的文档
  5. **指标**:跟踪授权率、延迟和被拒绝的资源
graph LR
    A[User Query] --> B[Base Retriever]
    B --> C[Top K Documents]
    C --> D[Extract Resource IDs]
    D --> E[SpiceDB Bulk Check]
    E --> F[Filter by Permissions]
    F --> G[Authorized Documents]

性能

检索器使用SpiceDB的原生 CheckBulkPermissionsRequest API以获得最佳性能:

  • 单个API调用:所有权限在一次请求中检查,而非N次单独调用
  • 高效:比单独的权限检查快得多
  • 可扩展:高效处理数百个文档

性能示例

# Retrieve 100 documents
base_docs = await base_retriever.ainvoke("query")
print(f"Retrieved: {len(base_docs)} documents")  # 100

# Filter with SpiceDB (single API call)
auth_docs = await auth_retriever.ainvoke("query")
print(f"Authorized: {len(auth_docs)} documents")  # e.g., 25

# All 100 permission checks happen in ~50ms (single bulk request)
# vs ~5000ms for 100 individual requests

错误处理

失败关闭(默认)

默认情况下,检索器采用失败关闭策略——如果检查权限时出错,文档将被过滤掉:

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    resource_id_key="article_id",
    permission="view",
    fail_open=False,  # Default - deny on errors
)

失败打开

用于开发或特定用例:

auth_retriever = SpiceDBRetriever(
    base_retriever=base_retriever,
    subject_id="alice",
    spicedb_endpoint="localhost:50051",
    spicedb_token="sometoken",
    resource_type="article",
    resource_id_key="article_id",
    permission="view",
    fail_open=True,  # Allow access on errors
)

完整示例:多用户RAG

from langchain_spicedb import SpiceDBRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document

# Setup
os.environ["OPENAI_API_KEY"] = "your-api-key"

# Create documents with metadata
documents = [
    Document(
        page_content="SpiceDB is an open-source authorization system.",
        metadata={"article_id": "doc1", "title": "Intro to SpiceDB"}
    ),
    Document(
        page_content="LangChain is a framework for LLM applications.",
        metadata={"article_id": "doc2", "title": "Intro to LangChain"}
    ),
    Document(
        page_content="Authorization is critical for RAG systems.",
        metadata={"article_id": "doc3", "title": "RAG Security"}
    ),
]

# Create vector store
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
base_retriever = vectorstore.as_retriever()

# Factory function for per-request retriever creation
def create_user_rag_chain(subject_id: str):
    """Create a RAG chain for a specific user (called per-request in production)."""
    auth_retriever = SpiceDBRetriever(
        base_retriever=base_retriever,
        subject_id=subject_id,
        spicedb_endpoint="localhost:50051",
        spicedb_token="sometoken",
        resource_type="article",
        resource_id_key="article_id",
        permission="view",
    )

    prompt = ChatPromptTemplate.from_messages([
        ("system", "Answer based only on the provided context."),
        ("human", "Question: {question}\n\nContext:\n{context}")
    ])

    def format_docs(docs):
        if not docs:
            return "No authorized documents found."
        return "\n\n".join(doc.page_content for doc in docs)

    return (
        {"context": auth_retriever | format_docs, "question": lambda x: x}
        | prompt
        | ChatOpenAI(model="gpt-4o-mini")
        | StrOutputParser()
    )

# Query different users (each request creates its own chain)
question = "What is SpiceDB?"

alice_answer = await create_user_rag_chain("alice").ainvoke(question)
print(f"Alice's answer: {alice_answer}")

bob_answer = await create_user_rag_chain("bob").ainvoke(question)
print(f"Bob's answer: {bob_answer}")

# In production web app:
# @app.post("/query")
# async def query(question: str, user_id: str):
#     chain = create_user_rag_chain(user_id)
#     return await chain.ainvoke(question)

# Different users see different documents and get different answers

API参考

SpiceDBRetriever

继承自: BaseRetriever

方法: - invoke(query: str) -> List[Document]:同步获取授权文档 - ainvoke(query: str) -> List[Document]:异步获取授权文档 - with_config(subject_id: str, **kwargs) -> SpiceDBRetriever:使用更新后的配置创建新的检索器

属性: - base_retriever:包装的检索器 - subject_id:当前用户ID - spicedb_endpoint:SpiceDB服务器地址 - resource_type:权限的资源类型 - permission:正在检查的权限

相关组件