Encrypted vector search for LangChain using Envector, powered by homomorphic encryption (CKKS). This repo ships a LangChain-compatible VectorStore and retriever utilities built on the high-level pyenvector Python SDK.
- LangChain
VectorStoreinterface withsimilarity_search,from_texts, etc. - Optional
VectorStoreRetrieverhelper for quick RAG integrations. - Client-side encryption handled transparently by the SDK, including score thresholds and filtering.
- In-place
delete,update_documentsandupsert_documentsby item ID, plus named partitions.
Requires pyenvector >= 1.6.2.
- Python 3.9–3.13 (recommend 3.11)
- Create and activate a virtualenv:
python3.11 -m venv .venv && source .venv/bin/activate
- Install runtime dependencies:
pip install -U pip setuptools wheelpip install 'pyenvector>=1.6.2' langchain sentence-transformers
- Configure Envector using
EnvectorConfig, pointing to your EnVector endpoint and keys. - Initialize embeddings (or provide pre-computed vectors).
- Instantiate
Envector(config=cfg, embeddings=emb)and calladd_texts,add_documents, or useas_retriever. - Run
similarity_searchor plug the retriever into your LangChain pipeline.
See
notebooks/for end-to-end walkthroughs and thelibs/envectorpackage for implementation details.
Key dataclasses live in libs/envector/config.py:
ConnectionConfig: address or host/port for EnVector; optionalkms_address/kms_secure/kms_ca_certfor the enVector KMS service. Whenkms_addressis set, keys are KMS-managed — omitKeyConfig.key_path.KeyConfig: key path, key ID, optional preset/eval mode.IndexSettings: index name, dimension (32–4096), query encryption mode, optional output fields and fetch parameters.WriteSettings: whether each write path waits for the server before returning. EnVector writes are asynchronous server-side, but what that means for the next read differs per operation: inserts are searchable immediately and do not wait, while updates return before the rebuilt rows are visible and do wait. Each default is documented with the measurement behind it.EnvectorConfig: wraps the above and enables auto-creation viacreate_if_missing.
- Each vector stores a single
metadatastring in EnVector. - To align with LangChain’s
Document, inserts wrap data as JSON:{"text": ..., "metadata": ...}. - Retrieval unwraps JSON, returning
Document(page_content=text, metadata={...}). - Client-side filtering requires the JSON envelope to include an object under
metadata.
- Item IDs are issued by the server (positive integers, returned as strings). Pass them back to
delete,update_documents,upsert_documents, or asidstoadd_documentsto update in place — any numeric id is taken to be one of them. Other IDs cannot be created; such rows get server-issued IDs and aUserWarning. - Fetch-by-ID (
get_by_ids) is unsupported, and so is LangChain'sindexingAPI, which depends on its own IDs. - Embeddings must be unit norm: scores are inner products computed under encryption, and vectors with components outside [-1, 1] rank incorrectly.
- A row deleted moments ago can still take a top-k slot briefly, so a search right after
deletemay return fewer thank; passfetch_kto over-fetch. - Filtering happens client-side after the server returns
khits, so filtered results can be fewer thank; setfetch_k(orIndexSettings.fetch_k) to over-fetch. - One enVector endpoint per process; all stores in a process must point at the same server.
- Updates wait for that store's pending inserts to merge first; when interleaving inserts and updates, set
WriteSettings.await_insert=Trueand use one store instance per index. update_documents/upsert_documentsabove 10,000 items are sent in several batches; if a later batch fails, the earlier ones stay applied.
from langchain_envector.config import ConnectionConfig, EnvectorConfig, IndexSettings, KeyConfig
cfg = EnvectorConfig(
connection=ConnectionConfig(
address=ENVECTOR_ADDRESS,
access_token=ENVECTOR_ACCESS_TOKEN
),
key=KeyConfig(
key_path=ENVECTOR_KEY_PATH,
key_id=ENVECTOR_KEY_ID,
preset="ip3",
eval_mode="mms32"
),
index=IndexSettings(
index_name=INDEX_NAME,
dim=vector_dim,
query_encryption="plain"
),
create_if_missing=True,
)from langchain_core.documents import Document
from langchain_envector.vectorstore import Envector
docs = [
Document(
page_content="chunk-1",
metadata={"source": "paper.pdf", "page": 1, "chunk": 0}
),
Document(
page_content="chunk-2",
metadata={"source": "paper.pdf", "page": 1, "chunk": 1}
),
]
store = Envector(config=cfg, embeddings=emb)
store.add_documents(docs)Or you can use add_texts to store vectors and their texts.
store.add_texts(
texts=["chunk 3"],
metadatas=[{"source": "paper.pdf", "page": 1, "chunk": 2}]
)results = store.similarity_search(query, k=1)
for doc in results:
print(f"* {doc.page_content} [{doc.metadata}]")results = store.similarity_search_with_score(query, k=1)
for doc, score in results:
print(f"* [SIM={score:.3f}] {doc.page_content} [{doc.metadata}]")query_embedding = embeddings.embed_query(query)
print(f"Query: {query_embedding[:3]}")
results = store.similarity_search_by_vector(query_embedding, k=3)
for doc in results:
print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")add_texts / add_documents return the item_id values EnVector assigned. Pass
them back to replace an item in place, preserving its ID (requires pyenvector >= 1.6.0):
ids = store.add_texts(["draft"], metadatas=[{"status": "draft"}])
# Replace both the vector and the stored payload: page_content is re-embedded.
result = store.update_documents(
ids, [Document(page_content="final", metadata={"status": "final"})]
)
print(result) # {"request_id": [...], "not_found_item_ids": [...]}IDs that match no live row (missing or already deleted) come back in
not_found_item_ids rather than raising.
For a metadata-only change that leaves the vector — and therefore what the item
matches — untouched, use update_metadata, or
update_documents(..., update_vectors=False):
store.update_metadata(ids, ["final"], metadatas=[{"status": "final"}])upsert_documents routes each document by whether it carries an ID: None
inserts, an existing item_id replaces in place. Note that a caller-chosen ID
cannot create a new item — an ID matching no live row is reported in
not_found_item_ids.
result = store.upsert_documents(
[Document(page_content="revised"), Document(page_content="brand new")],
ids=[ids[0], None],
)
print(result["inserted_item_ids"]) # IDs issued for the ID-less entriesstore.delete(ids) # accepts ints or numeric strings, e.g. doc.idDeletion is asynchronous server-side; by default delete waits for the SDK's
completion signal (WriteSettings.await_delete).
Named partitions isolate subsets of an index:
store.create_partition("tenant_a")
store.add_texts(["tenant-a data"], partition_name="tenant_a")
results = store.similarity_search(query, k=3, partition_names=["tenant_a"])
print(store.list_partitions()) # [{"name": ..., "status": ..., "num_vectors": ...}]
store.drop_partition("tenant_a") # removes the partition and its dataOmitting partition_name / partition_names uses the default partition or searches the whole index. Updates and deletes address rows within one partition, so pass partition_name for rows stored in a named partition.
- Connection issues: verify EnVector address and registered keys.
- Embeddings mismatch: ensure embedding dimension equals
index.dimwhen supplying vectors. - Unexpected raw strings: confirm inserts used the JSON envelope.
- Key Issues: check key's metadata to sync with the registered key if facing any key issue.
Before running tests, install dependencies for pytest:
pip install -r tests/requirements.txtRun unit tests offline (no EnVector or SDK required)
python -m pytest -q -m "not integration"
# or
python scripts/run_unit_tests.pyRun integration tests (requires enVector server)
-
Prepare the running enVector server
-
Export the environment variables:
ENVECTOR_ADDRESSENVECTOR_KEY_PATHENVECTOR_KEY_IDENVECTOR_INDEX_NAME- (Optional)
ENVECTOR_USE_EMBEDDINGS=1 - (Optional)
ENVECTOR_EMB_MODEL - (Optional)
ENVECTOR_USE_HF_DATASET=1
- Run the following command:
python -m pytest -q -m integration -sSee CONTRIBUTE.md for development, testing, and PR guidelines.