Developer tools · · 6 min read
Qdrant: search only the right customer’s published FAQs
Build and test a customer-scoped FAQ retrieval filter in Python, including draft records, another customer’s content and an empty result.
By Sociologix Editorial

Start with the records a chatbot may retrieve
A customer-care assistant can produce a plausible answer from the wrong source. Imagine two customers sharing a knowledge system: both have onboarding FAQs, but their approved instructions differ. A draft procedure adds a third version. Before asking a model to compose an answer, decide which records it may see.
This AI-assisted editorial guide tests one small part of that workflow: a Qdrant query that requires both the current customer’s identifier and a published status. The example is original, uses synthetic records and runs locally. It does not call an AI model, evaluate answer quality or establish complete application security.
1. Create an isolated Python environment
Qdrant’s official Python client supports an in-memory local mode without a running Qdrant server. It is useful for prototyping and tests. The following PowerShell commands create a separate environment; no cloud account, API key or model download is needed for this demonstration. [1]
The example was run on Windows with Python 3.13.15 and qdrant-client 1.19.1 on October 13, 2026. That package declares Python 3.10 or newer. Save the two Python blocks below, in order, as faq_filter_demo.py, then run the final command. The install command requires internet access. [2]
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install "qdrant-client==1.19.1"
# Save both Python blocks below as faq_filter_demo.py first.
.\.venv\Scripts\python.exe faq_filter_demo.py2. Seed deliberately conflicting FAQ records
Create four records: an approved FAQ for demo-a, its draft, an approved FAQ for demo-b and a record missing publication status. The text is intentionally minimal. Each vector is the same manually chosen three-number array, so similarity cannot hide a filtering mistake.
These are not embeddings and this is not semantic search. In a real pipeline, generate document and query vectors with the same embedding model and compatible dimensions. Keep this synthetic fixture as a separate regression test: it is easier to diagnose access-selection errors when relevance is held constant.
from qdrant_client import QdrantClient, models
client = QdrantClient(":memory:")
client.create_collection(
"faq_demo",
vectors_config=models.VectorParams(size=3, distance=models.Distance.COSINE),
)
records = [
(1, {"tenant_id": "demo-a", "status": "published", "text": "A approved FAQ"}),
(2, {"tenant_id": "demo-a", "status": "draft", "text": "A draft FAQ"}),
(3, {"tenant_id": "demo-b", "status": "published", "text": "B approved FAQ"}),
(4, {"tenant_id": "demo-a", "text": "A FAQ missing publication status"}),
]
client.upsert(
collection_name="faq_demo",
points=[models.PointStruct(id=i, vector=[1.0, 0.0, 0.0], payload=p)
for i, p in records],
)3. Require both customer and publication status
Qdrant payload filters let a search require field conditions. Conditions inside must all have to match; here both values use exact matches. The tenant_id and status names are our application’s metadata fields, not special built-in access roles. The query returns point identifiers and payloads so the assertions can inspect the selected records. [3]
Append this block to the same file. Set comparison deliberately ignores tie ordering because all four vectors are identical. The unfiltered query is a negative control used only in this local test.
# In production, resolve this value from verified server-side membership.
def search_for(tenant_id):
return client.query_points(
collection_name="faq_demo",
query=[1.0, 0.0, 0.0],
query_filter=models.Filter(must=[
models.FieldCondition(key="tenant_id", match=models.MatchValue(value=tenant_id)),
models.FieldCondition(key="status", match=models.MatchValue(value="published")),
]),
limit=10,
with_payload=True,
).points
for tenant, expected in [("demo-a", {1}), ("demo-b", {3}), ("unknown", set())]:
actual = {point.id for point in search_for(tenant)}
assert actual == expected, (tenant, actual)
print(tenant, sorted(actual))
# Negative control: without the filter, all four records are eligible.
unfiltered = client.query_points(
collection_name="faq_demo", query=[1.0, 0.0, 0.0], limit=10,
).points
assert {point.id for point in unfiltered} == {1, 2, 3, 4}
print("Unfiltered control:", sorted(point.id for point in unfiltered))
client.close()4. Check the observed result and the failure cases
The local run returned record 1 for demo-a, record 3 for demo-b and no records for unknown. The unfiltered control returned records 1, 2, 3 and 4. All four assertions passed. In this fixture, the draft and missing-status records were excluded, and each known customer received only its own published FAQ.
If the script fails, first check the package version, collection vector size and exact metadata spelling. A missing or misspelled status should be repaired during ingestion, rather than making the search more permissive. If an authorized search returns nothing, show a no-answer state or request clarification; do not retry without the filter.
5. Put the filter behind your application’s identity checks
The demo accepts a tenant string for testing. A production endpoint should resolve that identifier from the signed-in user’s verified membership on the server. Do not let a request body, URL parameter or model tool call select an arbitrary customer. Reject missing identity before searching, and apply the same policy to document previews, citations and downloads.
Qdrant documents payload-based partitioning in a shared collection as one multitenancy approach. The right deployment boundary depends on your isolation needs; this example does not decide that architecture. For server deployment, plan payload indexes for fields used in filters before loading data. Those indexes concern retrieval performance, not proof of authorization. [3][4]
Before launch, extend the regression suite to cover revoked membership, withdrawn documents, stale caches and a user attempting to request another customer’s identifier. Test the actual authenticated endpoint as well as its database query. Keep logs limited to identifiers and operational outcomes where possible; a successful filter test is a starting point for the full customer-care workflow.
Sources & further reading
Build a customer-care assistant around approved knowledge
Sociologix can help map your knowledge sources, customer access rules and escalation paths, then implement and test a retrieval workflow for your business.
Talk to Sociologix