From 37e6f5a211548c77813287ad0312f347e1b25ff7 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Tue, 26 May 2026 19:19:25 -0400 Subject: [PATCH 01/10] add Tutorial --- .../branch-aware-search.md | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md new file mode 100644 index 000000000..cd9b5dbed --- /dev/null +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -0,0 +1,275 @@ +--- +title: Branch-Aware Search +short_description: "Build branch-aware semantic search on Qdrant: query a corpus version, get back what's live on that branch." +description: "Index a versioned corpus in Qdrant and scope queries to a branch's HEAD using a materialized per-branch live-set of point IDs." +weight: 12 +aliases: + - /documentation/tutorials/branch-aware-search/ +--- + +# Branch-Aware Search Over Versioned Documents + +| Time: 30 min | Level: Intermediate | Output: [GitHub](https://github.com/qdrant/examples/tree/master/branch-aware-search) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://githubtocolab.com/qdrant/examples/blob/master/branch-aware-search/branch_aware_search.ipynb) | +| --- | ----------- | ----------- | ----------- | + +Searching a versioned corpus needs to scope to "what's live on this branch right now." Without that, a query on a feature branch returns content from `main` the branch has overridden, or misses content the branch added. This tutorial builds the pattern: materialize each branch's HEAD as a set of point IDs and filter queries against that set. Creating a new branch copies that set. No new embeddings, no new Qdrant writes. + +The pipeline applies anywhere a corpus has a branching history: IDE assistants searching a developer's feature branch, CMS systems searching draft content separately from published, policy or contract repositories with regional or jurisdictional variants. We use a synthetic mini-repo here so the mechanic is the focus. + +This tutorial assumes you're comfortable with [hybrid search](/documentation/search/text-search/#combining-semantic-and-lexical-search-with-hybrid-search), [filtering](/documentation/search/filtering/), and the [Query API](/documentation/search/hybrid-queries/). + +## Setup + +Install the Python packages used throughout the tutorial: + +```bash +pip install "qdrant-client>=1.18" +``` + + + +## The Synthetic Corpus + +The corpus is a small in-memory documentation site: pricing pages, policies, getting-started guides, and API reference. Three branches: `main` (the live published version), plus two draft branches forked from main, `pricing-refresh` (marketing's draft) and `compliance-update` (legal's draft). + +```python +main_docs = { + "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats.", + "pricing/enterprise.md": "Enterprise pricing is customized based on volume. Contact sales for a quote, custom SLAs, and an SSO demo.", + "pricing/free-tier.md": "The Free tier is permanently free with 1 GB of storage and 100 API calls per day. No credit card required.", + "policies/refunds.md": "Refunds are processed within 30 days of the original purchase. Pro-rated charges apply for partial periods.", + "policies/data-retention.md": "Customer data is retained for the duration of the contract and 90 days after termination, then permanently deleted.", + "policies/acceptable-use.md": "Our acceptable use policy prohibits scraping, automated abuse, and any content that violates applicable laws.", + "getting-started/install.md": "Install the CLI with pip install our-tool. The default config writes to ~/.our-tool/config.json.", + "getting-started/auth.md": "Authenticate with an API key from your dashboard. Set it via OUR_TOOL_API_KEY or in the config file.", + "getting-started/first-query.md":"Run your first query with our-tool query 'your search here'. Results return as JSON or YAML.", + "guides/python-sdk.md": "The Python SDK wraps every API endpoint. Install with pip install our-tool-sdk and import from our_tool.", + "guides/typescript-sdk.md": "The TypeScript SDK ships type definitions for all API responses. Install with npm install @our-tool/sdk.", + "guides/migrations.md": "Schema migrations run automatically on deployment. Roll back with our-tool migrate --rollback.", + "guides/monitoring.md": "Metrics export in Prometheus format on port 9090. Dashboard templates ship for Grafana.", + "guides/troubleshooting.md": "Common errors and their resolutions. Check the status page first if multiple users are affected.", + "guides/best-practices.md": "Production-grade recommendations for security, performance, and reliability.", + "api/overview.md": "The REST API uses standard HTTPS with bearer token auth. All responses are JSON.", + "api/rate-limits.md": "Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits.", + "api/errors.md": "All errors return standard HTTP status codes with a JSON body describing the cause.", + "README.md": "Overview of our tool for new users. Start with the Getting Started guide.", + "CHANGELOG.md": "Release notes by version, newest first. Major releases follow semantic versioning.", +} + +# Marketing draft: bump Pro tier pricing and refresh enterprise messaging. +pricing_refresh_overrides = { + "pricing/pro-tier.md": "The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support.", + "pricing/enterprise.md": "Enterprise pricing starts at $499 per month with custom volume discounts. SSO, audit logs, and dedicated support included.", +} + +# Compliance draft: add GDPR language to the Pro tier page and tighten the refund window. +compliance_update_overrides = { + "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage.", + "policies/refunds.md": "Refunds are processed within 14 days for Pro customers and 30 days for Free tier. No questions asked within the first 7 days.", +} +``` + +Both drafts edit `pricing/pro-tier.md` with different angles: `pricing-refresh` changes the price and adds SSO, `compliance-update` adds GDPR copy at the original price. Each also touches one additional file unique to it. Everything else is shared across all three branches. Same path, three branches, three distinct versions of `pricing/pro-tier.md`: that's the picture we want to verify. + +## Collection Schema and Branch State + +The content lives in a Qdrant collection. For simplicity, the branch graph and per-branch live-set live in plain Python dictionaries. + +```python +from qdrant_client import QdrantClient, models + +# Replace url and api_key with your own from https://cloud.qdrant.io +client = QdrantClient( + url="https://xyz-example.qdrant.io:6333", + api_key="", + cloud_inference=True, +) + +client.create_collection( + collection_name="content", + vectors_config={ + "dense": models.VectorParams(size=384, distance=models.Distance.COSINE), + }, + sparse_vectors_config={ + "bm25": models.SparseVectorParams(modifier=models.Modifier.IDF), + }, +) +``` + +Branch membership lives outside the point payload, so no payload indexes are needed. In this design, where content-derived point IDs are shared across branches, storing live membership on each point (scalar tag, array of branches, or similar) would conflict with that sharing: each branch's update would either overwrite another branch's data or require writes across every shared point. + +An alternative design that includes the branch in the point ID itself (e.g., `uuid5(NS, "branch|path|hash")`) would let branch info live on the point safely, at the cost of re-ingesting all of a parent's content on every fork. This tutorial trades that for cheap branching. + +Branch state in Python: + +```python +# Per-branch live-set: which point IDs are currently visible on this branch's HEAD +live_sets: dict[str, set[str]] = {} + +# Per-branch (path -> set of point IDs). The set keeps the structure correct +# for the chunked-file case even though this tutorial uses one chunk per file. +path_to_points: dict[str, dict[str, set[str]]] = {} +``` + + + +## Ingest + +Point IDs are derived from `(path, content_hash)`, so the same content at the same path always maps to the same point. Branches that share an unchanged file share the point too. + +```python +import hashlib +import uuid + +CONTENT_NS = uuid.UUID("00000000-0000-0000-0000-000000000001") + +def point_id(path: str, content: str) -> str: + """Deterministic UUID from (path, content): same inputs always produce the same ID.""" + content_hash = hashlib.sha256(content.encode()).hexdigest() + return str(uuid.uuid5(CONTENT_NS, f"{path}|{content_hash}")) + +def create_branch(name: str, parent: str | None): + """Create a branch. A fork copies the parent's live-set, so the child starts identical and diverges as it commits.""" + if parent is None: + live_sets[name] = set() + path_to_points[name] = {} + else: + live_sets[name] = set(live_sets[parent]) + path_to_points[name] = {p: set(ids) for p, ids in path_to_points[parent].items()} + +def ingest_commit(branch: str, files: dict[str, str]): + """Upsert a commit's files, then update the branch's live-set: evict each path's old point IDs and add the new ones.""" + points = [] + # New point IDs grouped by path (one per file here; a set so the multi-chunk case works too). + new_ids_by_path: dict[str, set[str]] = {} + for path, content in files.items(): + pid = point_id(path, content) + new_ids_by_path.setdefault(path, set()).add(pid) + points.append(models.PointStruct( + id=pid, + vector={ + "dense": models.Document(text=content, model="sentence-transformers/all-minilm-l6-v2"), + "bm25": models.Document(text=content, model="qdrant/bm25", options={"avg_len": 12}), + }, + payload={"path": path, "content": content}, + )) + + # Write to Qdrant first; only mutate branch state after the upsert is durably + # acknowledged. If the upsert fails the exception propagates and the in-memory + # branch HEAD is unchanged. + client.upsert(collection_name="content", points=points, wait=True) + + for path, new_ids in new_ids_by_path.items(): + prev_ids = path_to_points[branch].get(path, set()) + live_sets[branch] -= prev_ids + live_sets[branch] |= new_ids + path_to_points[branch][path] = new_ids + +def delete_files(branch: str, paths: list[str]): + """Delete files from a branch by dropping their point IDs from the live-set.""" + for path in paths: + ids = path_to_points[branch].pop(path, set()) + live_sets[branch] -= ids +``` + +Removing a file from a branch drops its point IDs from that branch's live-set, so queries on that branch stop returning it. The point itself stays in Qdrant: the tutorial never deletes points, so it remains both for other branches that reference it and after the last branch drops it (reclaiming those is the storage-growth concern in Scaling to Production). A filter-based design that walks ancestors would need a tombstone here, because the ancestor clause keeps matching the parent's copy. The live-set sidesteps that. + +Set up the fixture: + +```python +create_branch("main", parent=None) +ingest_commit("main", main_docs) + +create_branch("pricing-refresh", parent="main") # marketing draft forks from main's HEAD +ingest_commit("pricing-refresh", pricing_refresh_overrides) + +create_branch("compliance-update", parent="main") # compliance draft forks from main's HEAD as a sibling +ingest_commit("compliance-update", compliance_update_overrides) +``` + +Forking copied main's set, and each draft swapped in new point IDs only for the files it changed. Everything else stays shared: + +![Three branch live-sets (main, pricing-refresh, and compliance-update), each holding 20 point IDs. The two drafts forked from main and copied its set, then swapped in new point IDs only for the files they changed; all other files keep the same IDs as main. One Qdrant collection stores 24 physical points: seven versions of the three edited files plus 17 shared files.](/documentation/tutorials/branch-aware-search/branch-live-sets.png) + +After ingest: + +- Each live-set has 20 point IDs (one per document at that branch's HEAD). +- `main`'s 20 IDs include the original `pricing/pro-tier.md`, `pricing/enterprise.md`, and `policies/refunds.md`. +- `pricing-refresh`'s 20 IDs swap in the new pricing copy for `pricing/pro-tier.md` and the new enterprise copy for `pricing/enterprise.md`. The other 18 are shared with main. +- `compliance-update`'s 20 IDs swap in the GDPR-extended `pricing/pro-tier.md` and the tightened `policies/refunds.md`. The other 18 are shared with main. +- The Qdrant collection physically stores 24 points: 20 from main, plus four new versions written by the two drafts. Old versions stay in the collection but drop out of the branches that overrode them. + +## Query + +Branch-aware search is a single hybrid query restricted to the points in the branch's live-set, using Qdrant's [Has id](/documentation/search/filtering/#has-id) filter: + +```python +def search(branch: str, query: str, limit: int = 5): + branch_filter = models.Filter( + must=[models.HasIdCondition(has_id=list(live_sets[branch]))] + ) + return client.query_points( + collection_name="content", + prefetch=[ + models.Prefetch( + query=models.Document(text=query, model="sentence-transformers/all-minilm-l6-v2"), + using="dense", + filter=branch_filter, + limit=50, + ), + models.Prefetch( + query=models.Document(text=query, model="qdrant/bm25", options={"avg_len": 12}), + using="bm25", + filter=branch_filter, + limit=50, + ), + ], + query=models.FusionQuery(fusion=models.Fusion.RRF), + limit=limit, + ).points +``` + + + +Run the same query against all three branches and print each one's top result: + +```python +query = "Pro tier monthly cost" + +print("main:", search("main", query, limit=1)[0].payload["content"]) +print("pricing-refresh:", search("pricing-refresh", query, limit=1)[0].payload["content"]) +print("compliance-update:", search("compliance-update", query, limit=1)[0].payload["content"]) + +# Output: +# main: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. +# pricing-refresh: The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support. +# compliance-update: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage. +``` + +Same query, three branches, three answers. The drafts stay isolated: `pricing-refresh`'s result never mentions GDPR, and `compliance-update`'s never mentions SSO. + +## Adapting to Other Content Types + +This tutorial stores one chunk per document because each document is short. Real corpora are usually chunked, and the mechanic carries over: the live-set still holds point IDs, the query is still a Has id filter, and the point ID gains a chunk anchor, becoming `(path, anchor, content_hash)`, so chunks within a file get distinct IDs. Editing one chunk changes only that chunk's point ID: + +![A code file split by function. The point ID for one function is built as uuid5 of three parts: the path, the anchor (its AST symbol path), and the content_hash. Editing that function changes only its content_hash, so only its point ID changes; the unchanged functions keep their IDs and stay shared across branches.](/documentation/tutorials/branch-aware-search/chunk-id-mapping.png) + +The anchor has to be stable, like a heading path, an AST symbol path, or a clause number, so unchanged content keeps its ID across edits. Unstable anchors like line numbers or byte offsets shift on every insertion and force the whole file to re-embed. For how to split each content type, see the [chunking strategies course](/course/essentials/day-1/chunking-strategies/). + +On update, re-chunk the whole file and pass all of its chunks, not only the changed ones: eviction is path-scoped, so a partial update would drop the unchanged chunks from the live-set. Passing every chunk means re-submitting the unchanged ones, which still carry the same IDs. Set the upsert's [update mode](/documentation/manage-data/points/#update-mode) to `models.UpdateMode.INSERT_ONLY` so those existing IDs are skipped and only the new or changed chunks get written. + +## Scaling to Production + +This pattern uses the simplest Qdrant primitives on purpose. The inline Has id filter carries the branch's whole ID set on every query, which stays comfortable into the thousands of points per branch, enough for most corpora. A few spots still need hardening before production: + +- **Storage growth.** Every edit adds a new point and keeps the old one, so long-lived branches accumulate versions. Reclaiming superseded points is a production concern, much like log compaction. + +- **Durable branch state.** The live-sets here live in Python dictionaries for clarity. In production, back them with a database, or rebuild them from the version-control system, which is the real source of truth. + +- **Out of scope.** Three-way merges, force-push, and concurrent writers on a branch are each their own design decision and aren't needed to show the core mechanic. Treat Qdrant as the search index and git as the source of truth: if the index drifts, rebuild it. From 3280bf2a04ca52bdf27807a4046e971fa6c6ddfc Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Tue, 26 May 2026 19:19:33 -0400 Subject: [PATCH 02/10] add assets --- .../branch-aware-search/branch-live-sets.png | Bin 0 -> 49224 bytes .../branch-aware-search/chunk-id-mapping.png | Bin 0 -> 76636 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png create mode 100644 qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png diff --git a/qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png b/qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png new file mode 100644 index 0000000000000000000000000000000000000000..628995aa5f07a632a0984cf2e65e6ea5f2ca419b GIT binary patch literal 49224 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 literal 0 HcmV?d00001 diff --git a/qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png b/qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..0e062678bd955ff14332f03ffb70bce08f064cfb GIT binary patch literal 76636 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

>%!+!lP54Rawcd~@QGaSutVNw2G6&rhX+|(l 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 literal 0 HcmV?d00001 From 7b20d2ec9e99e4e2540b6ce09601e156569fc5b6 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Tue, 26 May 2026 19:19:47 -0400 Subject: [PATCH 03/10] add navigation links --- .../headless/content/tutorials/search-engineering.md | 3 ++- qdrant-landing/content/documentation/tutorials-lp-overview.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md b/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md index aeafa9d0e..34a88523e 100644 --- a/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md +++ b/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md @@ -6,4 +6,5 @@ | [Measuring ANN Recall](/documentation/tutorials-search-engineering/ann-recall/) | Measure ANN recall with the Web UI and tune HNSW parameters. | Web UI | 15m | Beginner | | [Multivectors and Late Interaction](/documentation/tutorials-search-engineering/using-multivector-representations/) | Effective use of multivector representations. | Python | 30m | Intermediate | | [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/) | Fuse title, summary, chunk, and tag vectors with named vectors and the Query API. | Python | 45m | Intermediate | -| [Static Embeddings](/documentation/tutorials-search-engineering/static-embeddings/) | Evaluate the utility of static embeddings. | Python | 20m | Intermediate | \ No newline at end of file +| [Static Embeddings](/documentation/tutorials-search-engineering/static-embeddings/) | Evaluate the utility of static embeddings. | Python | 20m | Intermediate | +| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope semantic search to a branch's HEAD in a versioned corpus. | Python | 30m | Intermediate | \ No newline at end of file diff --git a/qdrant-landing/content/documentation/tutorials-lp-overview.md b/qdrant-landing/content/documentation/tutorials-lp-overview.md index 5d3457377..4eefa7080 100644 --- a/qdrant-landing/content/documentation/tutorials-lp-overview.md +++ b/qdrant-landing/content/documentation/tutorials-lp-overview.md @@ -76,6 +76,7 @@ partition: develop | [Semantic Search for Code](/documentation/tutorials-search-engineering/code-search/) | Navigate codebases using vector similarity. | Python | 45m | Intermediate | | [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/) | Fuse title, summary, chunk, and tag vectors with named vectors and the Query API. | Python | 45m | Intermediate | | [Static Embeddings](/documentation/tutorials-search-engineering/static-embeddings/) | Evaluate the renaissance of static embeddings. | Python | 20m | Intermediate | +| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope semantic search to a branch's HEAD in a versioned corpus. | Python | 30m | Intermediate | --- From 26ea463cf185aa4122ee3c42e98a9a9253f3379d Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Thu, 28 May 2026 00:19:34 -0400 Subject: [PATCH 04/10] new design --- .../content/tutorials/search-engineering.md | 2 +- .../documentation/tutorials-lp-overview.md | 2 +- .../branch-aware-search.md | 400 +++++++++++------- .../branch-aware-search/branch-live-sets.png | Bin 49224 -> 0 bytes .../branch-aware-search/chunk-id-mapping.png | Bin 76636 -> 0 bytes 5 files changed, 242 insertions(+), 162 deletions(-) delete mode 100644 qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png delete mode 100644 qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png diff --git a/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md b/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md index 34a88523e..198f7acb4 100644 --- a/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md +++ b/qdrant-landing/content/documentation/headless/content/tutorials/search-engineering.md @@ -7,4 +7,4 @@ | [Multivectors and Late Interaction](/documentation/tutorials-search-engineering/using-multivector-representations/) | Effective use of multivector representations. | Python | 30m | Intermediate | | [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/) | Fuse title, summary, chunk, and tag vectors with named vectors and the Query API. | Python | 45m | Intermediate | | [Static Embeddings](/documentation/tutorials-search-engineering/static-embeddings/) | Evaluate the utility of static embeddings. | Python | 20m | Intermediate | -| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope semantic search to a branch's HEAD in a versioned corpus. | Python | 30m | Intermediate | \ No newline at end of file +| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope search to a branch's live view in a versioned corpus, inherited from its ancestors. | Python | 25m | Intermediate | \ No newline at end of file diff --git a/qdrant-landing/content/documentation/tutorials-lp-overview.md b/qdrant-landing/content/documentation/tutorials-lp-overview.md index 4eefa7080..50d4f7183 100644 --- a/qdrant-landing/content/documentation/tutorials-lp-overview.md +++ b/qdrant-landing/content/documentation/tutorials-lp-overview.md @@ -76,7 +76,7 @@ partition: develop | [Semantic Search for Code](/documentation/tutorials-search-engineering/code-search/) | Navigate codebases using vector similarity. | Python | 45m | Intermediate | | [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/) | Fuse title, summary, chunk, and tag vectors with named vectors and the Query API. | Python | 45m | Intermediate | | [Static Embeddings](/documentation/tutorials-search-engineering/static-embeddings/) | Evaluate the renaissance of static embeddings. | Python | 20m | Intermediate | -| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope semantic search to a branch's HEAD in a versioned corpus. | Python | 30m | Intermediate | +| [Branch-Aware Search](/documentation/tutorials-search-engineering/branch-aware-search/) | Scope search to a branch's live view in a versioned corpus, inherited from its ancestors. | Python | 25m | Intermediate | --- diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index cd9b5dbed..34231c7c8 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -1,7 +1,7 @@ --- title: Branch-Aware Search -short_description: "Build branch-aware semantic search on Qdrant: query a corpus version, get back what's live on that branch." -description: "Index a versioned corpus in Qdrant and scope queries to a branch's HEAD using a materialized per-branch live-set of point IDs." +short_description: "Scope search to a version-controlled branch in Qdrant so a query returns that branch's live view, inherited from its ancestors." +description: "Index a versioned corpus in Qdrant and scope queries to a branch's live view with an ancestor-walk filter over per-version branch and seq payload fields." weight: 12 aliases: - /documentation/tutorials/branch-aware-search/ @@ -9,18 +9,16 @@ aliases: # Branch-Aware Search Over Versioned Documents -| Time: 30 min | Level: Intermediate | Output: [GitHub](https://github.com/qdrant/examples/tree/master/branch-aware-search) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://githubtocolab.com/qdrant/examples/blob/master/branch-aware-search/branch_aware_search.ipynb) | -| --- | ----------- | ----------- | ----------- | +| Time: 25 min | Level: Intermediate | +| --- | ----------- | -Searching a versioned corpus needs to scope to "what's live on this branch right now." Without that, a query on a feature branch returns content from `main` the branch has overridden, or misses content the branch added. This tutorial builds the pattern: materialize each branch's HEAD as a set of point IDs and filter queries against that set. Creating a new branch copies that set. No new embeddings, no new Qdrant writes. +A search on a specific branch should return exactly the content that is live on that branch, and nothing from another. Without that scoping, a query returns content from other branches, or a version this branch already replaced. -The pipeline applies anywhere a corpus has a branching history: IDE assistants searching a developer's feature branch, CMS systems searching draft content separately from published, policy or contract repositories with regional or jurisdictional variants. We use a synthetic mini-repo here so the mechanic is the focus. - -This tutorial assumes you're comfortable with [hybrid search](/documentation/search/text-search/#combining-semantic-and-lexical-search-with-hybrid-search), [filtering](/documentation/search/filtering/), and the [Query API](/documentation/search/hybrid-queries/). +Each version of a file is one point in the Qdrant collection, tagged with the `branch` that wrote it and a `seq` (the commit number on that branch).
When a later commit overwrites or deletes a file, the previous version's point records which branch did it, and when. A search on a branch is then a single filter: it reads from that branch and the branches it forked from, and skips anything they later replaced. ## Setup -Install the Python packages used throughout the tutorial: +Install the Python client: ```bash pip install "qdrant-client>=1.18" @@ -32,50 +30,92 @@ This tutorial uses Qd ## The Synthetic Corpus -The corpus is a small in-memory documentation site: pricing pages, policies, getting-started guides, and API reference. Three branches: `main` (the live published version), plus two draft branches forked from main, `pricing-refresh` (marketing's draft) and `compliance-update` (legal's draft). +The corpus is a small documentation site: pricing pages, policies, getting-started guides, and API reference. There are three branches: `main` is the published version, with `pricing-refresh` (marketing's) and `compliance-update` (legal's) forked from it. ```python main_docs = { - "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats.", - "pricing/enterprise.md": "Enterprise pricing is customized based on volume. Contact sales for a quote, custom SLAs, and an SSO demo.", - "pricing/free-tier.md": "The Free tier is permanently free with 1 GB of storage and 100 API calls per day. No credit card required.", - "policies/refunds.md": "Refunds are processed within 30 days of the original purchase. Pro-rated charges apply for partial periods.", - "policies/data-retention.md": "Customer data is retained for the duration of the contract and 90 days after termination, then permanently deleted.", - "policies/acceptable-use.md": "Our acceptable use policy prohibits scraping, automated abuse, and any content that violates applicable laws.", - "getting-started/install.md": "Install the CLI with pip install our-tool. The default config writes to ~/.our-tool/config.json.", - "getting-started/auth.md": "Authenticate with an API key from your dashboard. Set it via OUR_TOOL_API_KEY or in the config file.", - "getting-started/first-query.md":"Run your first query with our-tool query 'your search here'. Results return as JSON or YAML.", - "guides/python-sdk.md": "The Python SDK wraps every API endpoint. Install with pip install our-tool-sdk and import from our_tool.", - "guides/typescript-sdk.md": "The TypeScript SDK ships type definitions for all API responses. Install with npm install @our-tool/sdk.", - "guides/migrations.md": "Schema migrations run automatically on deployment. Roll back with our-tool migrate --rollback.", - "guides/monitoring.md": "Metrics export in Prometheus format on port 9090. Dashboard templates ship for Grafana.", - "guides/troubleshooting.md": "Common errors and their resolutions. Check the status page first if multiple users are affected.", - "guides/best-practices.md": "Production-grade recommendations for security, performance, and reliability.", - "api/overview.md": "The REST API uses standard HTTPS with bearer token auth. All responses are JSON.", - "api/rate-limits.md": "Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits.", - "api/errors.md": "All errors return standard HTTP status codes with a JSON body describing the cause.", - "README.md": "Overview of our tool for new users. Start with the Getting Started guide.", - "CHANGELOG.md": "Release notes by version, newest first. Major releases follow semantic versioning.", + "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats.", + "pricing/enterprise.md": "Enterprise pricing is customized based on volume. Contact sales for a quote, custom SLAs, and an SSO demo.", + "pricing/free-tier.md": "The Free tier is permanently free with 1 GB of storage and 100 API calls per day. No credit card required.", + "policies/refunds.md": "Refunds are processed within 30 days of the original purchase. Pro-rated charges apply for partial periods.", + "policies/data-retention.md": "Customer data is retained for the duration of the contract and 90 days after termination, then permanently deleted.", + "policies/acceptable-use.md": "Our acceptable use policy prohibits scraping, automated abuse, and any content that violates applicable laws.", + "getting-started/install.md": "Install the CLI with pip install our-tool. The default config writes to ~/.our-tool/config.json.", + "getting-started/auth.md": "Authenticate with an API key from your dashboard. Set it via OUR_TOOL_API_KEY or in the config file.", + "getting-started/first-query.md": "Run your first query with our-tool query 'your search here'. Results return as JSON or YAML.", + "guides/python-sdk.md": "The Python SDK wraps every API endpoint. Install with pip install our-tool-sdk and import from our_tool.", + "guides/typescript-sdk.md": "The TypeScript SDK ships type definitions for all API responses. Install with npm install @our-tool/sdk.", + "guides/migrations.md": "Schema migrations run automatically on deployment. Roll back with our-tool migrate --rollback.", + "guides/monitoring.md": "Metrics export in Prometheus format on port 9090. Dashboard templates ship for Grafana.", + "guides/troubleshooting.md": "Common errors and their resolutions. Check the status page first if multiple users are affected.", + "guides/best-practices.md": "Production-grade recommendations for security, performance, and reliability.", + "api/overview.md": "The REST API uses standard HTTPS with bearer token auth. All responses are JSON.", + "api/rate-limits.md": "Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits.", + "api/errors.md": "All errors return standard HTTP status codes with a JSON body describing the cause.", + "README.md": "Overview of our tool for new users. Start with the Getting Started guide.", + "CHANGELOG.md": "Release notes by version, newest first. Major releases follow semantic versioning.", } -# Marketing draft: bump Pro tier pricing and refresh enterprise messaging. -pricing_refresh_overrides = { - "pricing/pro-tier.md": "The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support.", - "pricing/enterprise.md": "Enterprise pricing starts at $499 per month with custom volume discounts. SSO, audit logs, and dedicated support included.", +# Marketing branch: raise the Pro price and refresh enterprise messaging. +pricing_refresh = { + "pricing/pro-tier.md": "The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support.", + "pricing/enterprise.md": "Enterprise pricing starts at $499 per month with custom volume discounts. SSO, audit logs, and dedicated support included.", } -# Compliance draft: add GDPR language to the Pro tier page and tighten the refund window. -compliance_update_overrides = { - "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage.", - "policies/refunds.md": "Refunds are processed within 14 days for Pro customers and 30 days for Free tier. No questions asked within the first 7 days.", +# Compliance branch: add GDPR language to the Pro tier page and tighten the refund window. +compliance_update = { + "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage.", + "policies/refunds.md": "Refunds are processed within 14 days for Pro customers and 30 days for Free tier. No questions asked within the first 7 days.", +} + +# main keeps committing after the two branches fork off: it edits a shared file and adds a new one. +main_later_edit = { + "api/rate-limits.md": "Free plan is 60 requests per minute. Pro and Enterprise are unlimited within fair use, with burst credits.", + "policies/sla.md": "Service level agreement: 99.9% uptime for Pro and Enterprise, measured monthly with service credits for breaches.", } ``` -Both drafts edit `pricing/pro-tier.md` with different angles: `pricing-refresh` changes the price and adds SSO, `compliance-update` adds GDPR copy at the original price. Each also touches one additional file unique to it. Everything else is shared across all three branches. Same path, three branches, three distinct versions of `pricing/pro-tier.md`: that's the picture we want to verify. +Two cases this design has to get right: -## Collection Schema and Branch State +1. **Per-branch versions of the same file.** Both forks edit `pricing/pro-tier.md`: `pricing-refresh` raises the price and adds SSO, while `compliance-update` adds GDPR copy at the original price. The same path should resolve to three different versions across the three branches. +2. **Timing.** After both branches fork off `main`, `main` keeps moving: it edits the shared `api/rate-limits.md` and adds `policies/sla.md`. Each fork should still see `main`'s files as they were at the fork point, not these later changes. -The content lives in a Qdrant collection. For simplicity, the branch graph and per-branch live-set live in plain Python dictionaries. +## How a Branch Sees Content + +A branch records its fork point as `(parent, parent's seq at fork)`. From there, what the branch sees is its own commits plus everything inherited from its ancestors up to the fork point, minus anything superseded along the way: + +```text +main ──● seq0 ───────────────────● seq1 + │ (20 base files) (edit api/rate-limits.md, add policies/sla.md) + ├── pricing-refresh ● seq0 + └── compliance-update ● seq0 +``` + +Both branches forked at `main seq0`, so each sees `main` as it stood at that fork. + +Three pieces of runtime state encode this in plain Python: + +```python +lineage: dict[str, tuple[str, int] | None] = {} # branch -> (parent, fork_seq) +last_seq: dict[str, int] = {} # branch -> its latest commit seq (-1 before first) +head: dict[str, dict[str, str]] = {} # branch -> {path -> current point id} +``` + +`lineage` is the durable per-branch record.
`head` and `last_seq` are replay bookkeeping, rebuilt from version control on startup. + +

+ +## Collection Schema + +Each point in the collection has three payload fields: + +- `branch`: the branch that wrote a version. +- `seq`: a per-branch counter we assign on each commit (`0`, `1`, `2`, ...), not the git commit hash. It orders each branch's commits, so the filter can include only the commits at or before each ancestor's fork point. +- `overwritten_in`: a list of `{by, seq}` records, each meaning "replaced in branch `by` at that branch's commit `seq`." Overwrites and deletes both append one. + +Every field the visibility filter touches needs a payload index: ```python from qdrant_client import QdrantClient, models @@ -89,162 +129,193 @@ client = QdrantClient( client.create_collection( collection_name="content", - vectors_config={ - "dense": models.VectorParams(size=384, distance=models.Distance.COSINE), - }, sparse_vectors_config={ "bm25": models.SparseVectorParams(modifier=models.Modifier.IDF), }, ) + +client.create_payload_index( + collection_name="content", + field_name="branch", + field_schema=models.PayloadSchemaType.KEYWORD, +) +client.create_payload_index( + collection_name="content", + field_name="seq", + field_schema=models.PayloadSchemaType.INTEGER, +) +client.create_payload_index( + collection_name="content", + field_name="overwritten_in[].by", + field_schema=models.PayloadSchemaType.KEYWORD, +) +client.create_payload_index( + collection_name="content", + field_name="overwritten_in[].seq", + field_schema=models.PayloadSchemaType.INTEGER, +) ``` -Branch membership lives outside the point payload, so no payload indexes are needed. In this design, where content-derived point IDs are shared across branches, storing live membership on each point (scalar tag, array of branches, or similar) would conflict with that sharing: each branch's update would either overwrite another branch's data or require writes across every shared point. - -An alternative design that includes the branch in the point ID itself (e.g., `uuid5(NS, "branch|path|hash")`) would let branch info live on the point safely, at the cost of re-ingesting all of a parent's content on every fork. This tutorial trades that for cheap branching. - -Branch state in Python: - -```python -# Per-branch live-set: which point IDs are currently visible on this branch's HEAD -live_sets: dict[str, set[str]] = {} - -# Per-branch (path -> set of point IDs). The set keeps the structure correct -# for the chunked-file case even though this tutorial uses one chunk per file. -path_to_points: dict[str, dict[str, set[str]]] = {} -``` - - - ## Ingest -Point IDs are derived from `(path, content_hash)`, so the same content at the same path always maps to the same point. Branches that share an unchanged file share the point too. +A commit writes a new point for each file it adds or changes, tagged with the branch and its current `seq`. If the file already had a version on this branch, that prior point gets a `{by, seq}` appended to the `overwritten_in` field, marking it superseded. +
The point ID is derived from `(branch, seq, path)`, so replaying the same history produces the same IDs and the rebuild path can `upsert` without duplicating points. + +
```python -import hashlib import uuid -CONTENT_NS = uuid.UUID("00000000-0000-0000-0000-000000000001") +NS = uuid.UUID("00000000-0000-0000-0000-000000000042") -def point_id(path: str, content: str) -> str: - """Deterministic UUID from (path, content): same inputs always produce the same ID.""" - content_hash = hashlib.sha256(content.encode()).hexdigest() - return str(uuid.uuid5(CONTENT_NS, f"{path}|{content_hash}")) +# BM25's key parameter for short fields: the average word count. +word_counts = [len(text.split()) for text in main_docs.values()] +AVG_LEN = round(sum(word_counts) / len(word_counts), 1) # ~15.3 here + +def point_id(branch: str, seq: int, path: str) -> str: + return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}")) def create_branch(name: str, parent: str | None): - """Create a branch. A fork copies the parent's live-set, so the child starts identical and diverges as it commits.""" + """Record a branch. A fork inherits the parent's view, no points written.""" + last_seq[name] = -1 if parent is None: - live_sets[name] = set() - path_to_points[name] = {} + lineage[name] = None + head[name] = {} else: - live_sets[name] = set(live_sets[parent]) - path_to_points[name] = {p: set(ids) for p, ids in path_to_points[parent].items()} + # fork point: parent + parent's latest seq + lineage[name] = (parent, last_seq[parent]) + head[name] = dict(head[parent]) # start from parent's view -def ingest_commit(branch: str, files: dict[str, str]): - """Upsert a commit's files, then update the branch's live-set: evict each path's old point IDs and add the new ones.""" +def supersede(branch: str, path: str, at_seq: int): + """Mark the version of `path` that `branch` currently sees, at `at_seq`.""" + prev = head[branch].get(path) + if prev is None: + return + point = client.retrieve("content", ids=[prev])[0] + marks = point.payload["overwritten_in"] + marks.append({"by": branch, "seq": at_seq}) + client.set_payload( + "content", + payload={"overwritten_in": marks}, + points=[prev], + ) + +def commit(branch: str, writes: dict[str, str] | None = None, deletes: list[str] | None = None): + """One commit at one seq: write or overwrite files, and/or delete files.""" + writes, deletes = writes or {}, deletes or [] + seq = last_seq[branch] + 1 + last_seq[branch] = seq points = [] - # New point IDs grouped by path (one per file here; a set so the multi-chunk case works too). - new_ids_by_path: dict[str, set[str]] = {} - for path, content in files.items(): - pid = point_id(path, content) - new_ids_by_path.setdefault(path, set()).add(pid) + for path, content in writes.items(): + supersede(branch, path, seq) # mark the version this commit replaces + pid = point_id(branch, seq, path) points.append(models.PointStruct( id=pid, vector={ - "dense": models.Document(text=content, model="sentence-transformers/all-minilm-l6-v2"), - "bm25": models.Document(text=content, model="qdrant/bm25", options={"avg_len": 12}), + "bm25": models.Document( + text=content, + model="qdrant/bm25", + options={"avg_len": AVG_LEN}, + ), + }, + payload={ + "path": path, + "content": content, + "branch": branch, + "seq": seq, + "overwritten_in": [], }, - payload={"path": path, "content": content}, )) - - # Write to Qdrant first; only mutate branch state after the upsert is durably - # acknowledged. If the upsert fails the exception propagates and the in-memory - # branch HEAD is unchanged. - client.upsert(collection_name="content", points=points, wait=True) - - for path, new_ids in new_ids_by_path.items(): - prev_ids = path_to_points[branch].get(path, set()) - live_sets[branch] -= prev_ids - live_sets[branch] |= new_ids - path_to_points[branch][path] = new_ids - -def delete_files(branch: str, paths: list[str]): - """Delete files from a branch by dropping their point IDs from the live-set.""" - for path in paths: - ids = path_to_points[branch].pop(path, set()) - live_sets[branch] -= ids + head[branch][path] = pid + for path in deletes: + supersede(branch, path, seq) # same record, no replacement point + head[branch].pop(path, None) + if points: + client.upsert(collection_name="content", points=points, wait=True) ``` -Removing a file from a branch drops its point IDs from that branch's live-set, so queries on that branch stop returning it. The point itself stays in Qdrant: the tutorial never deletes points, so it remains both for other branches that reference it and after the last branch drops it (reclaiming those is the storage-growth concern in Scaling to Production). A filter-based design that walks ancestors would need a tombstone here, because the ancestor clause keeps matching the parent's copy. The live-set sidesteps that. +A delete writes the same record as an overwrite, with no replacement point behind it, and it is a real commit, so it carries a `seq` like any other. That `seq` is what lets a branch that forked before the delete keep the file, while the branch that deleted it (and anything forked after) does not. -Set up the fixture: +Build the fixture: ```python create_branch("main", parent=None) -ingest_commit("main", main_docs) +commit("main", writes=main_docs) -create_branch("pricing-refresh", parent="main") # marketing draft forks from main's HEAD -ingest_commit("pricing-refresh", pricing_refresh_overrides) +create_branch("pricing-refresh", parent="main") # forks at main seq0 +commit("pricing-refresh", writes=pricing_refresh) -create_branch("compliance-update", parent="main") # compliance draft forks from main's HEAD as a sibling -ingest_commit("compliance-update", compliance_update_overrides) +create_branch("compliance-update", parent="main") # also at main seq0 +commit( + "compliance-update", + writes=compliance_update, + deletes=["policies/acceptable-use.md"], +) + +commit("main", writes=main_later_edit) # main seq1, after both forks ``` -Forking copied main's set, and each draft swapped in new point IDs only for the files it changed. Everything else stays shared: - -![Three branch live-sets (main, pricing-refresh, and compliance-update), each holding 20 point IDs. The two drafts forked from main and copied its set, then swapped in new point IDs only for the files they changed; all other files keep the same IDs as main. One Qdrant collection stores 24 physical points: seven versions of the three edited files plus 17 shared files.](/documentation/tutorials/branch-aware-search/branch-live-sets.png) - -After ingest: - -- Each live-set has 20 point IDs (one per document at that branch's HEAD). -- `main`'s 20 IDs include the original `pricing/pro-tier.md`, `pricing/enterprise.md`, and `policies/refunds.md`. -- `pricing-refresh`'s 20 IDs swap in the new pricing copy for `pricing/pro-tier.md` and the new enterprise copy for `pricing/enterprise.md`. The other 18 are shared with main. -- `compliance-update`'s 20 IDs swap in the GDPR-extended `pricing/pro-tier.md` and the tightened `policies/refunds.md`. The other 18 are shared with main. -- The Qdrant collection physically stores 24 points: 20 from main, plus four new versions written by the two drafts. Old versions stay in the collection but drop out of the branches that overrode them. +Each fork wrote only the files it changed, and `main`'s later commit wrote two points (the edited `api/rate-limits.md` and the new `policies/sla.md`). The collection holds 26 physical points: the 20 base files, two versions from each fork, and the two from `main`'s later commit. ## Query -Branch-aware search is a single hybrid query restricted to the points in the branch's live-set, using Qdrant's [Has id](/documentation/search/filtering/#has-id) filter: +A query on a branch is one filter built from a lineage walk.
For each branch on the path there's a **cutoff**, the highest `seq` to include from that branch: for the querying branch itself, that's its latest commit; for an ancestor, the `seq` at which this line forked from it.
Both filter clauses key off those cutoffs. The `should` clause (the candidates to consider) gathers each branch's versions up to its cutoff. The `must_not` clause (the ones to drop) excludes anything that branch superseded at or before its cutoff, using a [nested filter](/documentation/search/filtering/#nested-object-filter) so `by` and `seq` match on the same record: ```python +def candidate_clause(b: str, cut: int) -> models.Filter: + """Versions written on branch b with seq up to cut.""" + return models.Filter(must=[ + models.FieldCondition(key="branch", match=models.MatchValue(value=b)), + models.FieldCondition(key="seq", range=models.Range(lte=cut)), + ]) + +def exclusion_clause(b: str, cut: int) -> models.NestedCondition: + """Match if overwritten_in has {by: b, seq <= cut} on the same record.""" + return models.NestedCondition(nested=models.Nested( + key="overwritten_in", + filter=models.Filter(must=[ + models.FieldCondition(key="by", match=models.MatchValue(value=b)), + models.FieldCondition(key="seq", range=models.Range(lte=cut)), + ]), + )) + +def visibility_filter(branch: str) -> models.Filter: + # Walk lineage: (branch, cutoff) for this branch then each ancestor. + cutoffs = [(branch, last_seq[branch])] + node = lineage[branch] + while node is not None: + parent, fork = node + cutoffs.append((parent, fork)) + node = lineage[parent] + + should, must_not = [], [] + for b, cut in cutoffs: + should.append(candidate_clause(b, cut)) + must_not.append(exclusion_clause(b, cut)) + return models.Filter(should=should, must_not=must_not) + def search(branch: str, query: str, limit: int = 5): - branch_filter = models.Filter( - must=[models.HasIdCondition(has_id=list(live_sets[branch]))] - ) return client.query_points( collection_name="content", - prefetch=[ - models.Prefetch( - query=models.Document(text=query, model="sentence-transformers/all-minilm-l6-v2"), - using="dense", - filter=branch_filter, - limit=50, - ), - models.Prefetch( - query=models.Document(text=query, model="qdrant/bm25", options={"avg_len": 12}), - using="bm25", - filter=branch_filter, - limit=50, - ), - ], - query=models.FusionQuery(fusion=models.Fusion.RRF), + query=models.Document( + text=query, + model="qdrant/bm25", + options={"avg_len": AVG_LEN}, + ), + using="bm25", + query_filter=visibility_filter(branch), limit=limit, + with_payload=True, ).points ``` - - -Run the same query against all three branches and print each one's top result: +Run a query that lands on `pricing/pro-tier.md`, against all three branches: ```python -query = "Pro tier monthly cost" - -print("main:", search("main", query, limit=1)[0].payload["content"]) -print("pricing-refresh:", search("pricing-refresh", query, limit=1)[0].payload["content"]) -print("compliance-update:", search("compliance-update", query, limit=1)[0].payload["content"]) +for branch in ["main", "pricing-refresh", "compliance-update"]: + print(f"{branch}:", search(branch, "Pro tier storage and seats", limit=1)[0].payload["content"]) # Output: # main: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. @@ -252,24 +323,33 @@ print("compliance-update:", search("compliance-update", query, limit=1)[0].paylo # compliance-update: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage. ``` -Same query, three branches, three answers. The drafts stay isolated: `pricing-refresh`'s result never mentions GDPR, and `compliance-update`'s never mentions SSO. +Same query, three branches, three different answers. `pricing-refresh` returns its $39 page with SSO. `compliance-update` returns its GDPR variant. `main` returns the original. -## Adapting to Other Content Types +Now the timing case. `main` edited `api/rate-limits.md` at `seq 1`, after both branches forked. `main` now shows the new text; the forks still see the version they inherited: -This tutorial stores one chunk per document because each document is short. Real corpora are usually chunked, and the mechanic carries over: the live-set still holds point IDs, the query is still a Has id filter, and the point ID gains a chunk anchor, becoming `(path, anchor, content_hash)`, so chunks within a file get distinct IDs. Editing one chunk changes only that chunk's point ID: +```python +for branch in ["main", "pricing-refresh"]: + print(f"{branch}:", search(branch, "rate limits requests free plan", limit=1)[0].payload["content"]) -![A code file split by function. The point ID for one function is built as uuid5 of three parts: the path, the anchor (its AST symbol path), and the content_hash. Editing that function changes only its content_hash, so only its point ID changes; the unchanged functions keep their IDs and stay shared across branches.](/documentation/tutorials/branch-aware-search/chunk-id-mapping.png) +# Output: +# main: Free plan is 60 requests per minute. Pro and Enterprise are unlimited within fair use, with burst credits. +# pricing-refresh: Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits. +``` -The anchor has to be stable, like a heading path, an AST symbol path, or a clause number, so unchanged content keeps its ID across edits. Unstable anchors like line numbers or byte offsets shift on every insertion and force the whole file to re-embed. For how to split each content type, see the [chunking strategies course](/course/essentials/day-1/chunking-strategies/). - -On update, re-chunk the whole file and pass all of its chunks, not only the changed ones: eviction is path-scoped, so a partial update would drop the unchanged chunks from the live-set. Passing every chunk means re-submitting the unchanged ones, which still carry the same IDs. Set the upsert's [update mode](/documentation/manage-data/points/#update-mode) to `models.UpdateMode.INSERT_ONLY` so those existing IDs are skipped and only the new or changed chunks get written. +`pricing-refresh` forked at `main`'s `seq 0`, before the `seq 1` edit. Its cutoff for `main` is `0`, so the record stamped at `seq 1` falls past the cutoff, the exclusion doesn't apply, and the pre-edit version stays visible. The same cutoff hides `policies/sla.md` (which `main` added at `seq 1`) from both forks. ## Scaling to Production -This pattern uses the simplest Qdrant primitives on purpose. The inline Has id filter carries the branch's whole ID set on every query, which stays comfortable into the thousands of points per branch, enough for most corpora. A few spots still need hardening before production: +- **The filter grows with lineage depth, not corpus size.** The query walks the lineage once, adding one `should` clause and one `must_not` clause per ancestor. The filter's size tracks how deep a branch sits, not how many files or versions the collection holds. -- **Storage growth.** Every edit adds a new point and keeps the old one, so long-lived branches accumulate versions. Reclaiming superseded points is a production concern, much like log compaction. +- **Qdrant is a derived index.** Branch lineage and history live in version control. Build the collection by replaying that history, and if the index ever drifts, rebuild it. Nothing here needs the index to be the source of truth. -- **Durable branch state.** The live-sets here live in Python dictionaries for clarity. In production, back them with a database, or rebuild them from the version-control system, which is the real source of truth. +- **Storage grows with edits.** Every overwrite keeps the old version as a point, so long-lived branches accumulate versions. Reclaiming superseded versions is a production concern. -- **Out of scope.** Three-way merges, force-push, and concurrent writers on a branch are each their own design decision and aren't needed to show the core mechanic. Treat Qdrant as the search index and git as the source of truth: if the index drifts, rebuild it. +- **Out of scope.** Three-way merges, force-push, and concurrent writers on a branch are each their own design decision and are not needed to show the core mechanic. + +## Related Reading + +- [Filtering](/documentation/search/filtering/) for the full set of filter clauses. +- [Text Search](/documentation/search/text-search/) for BM25 and the `avg_len` parameter. +- [Payload Indexing](/documentation/manage-data/indexing/#payload-index) for the index types the visibility fields use. diff --git a/qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png b/qdrant-landing/static/documentation/tutorials/branch-aware-search/branch-live-sets.png deleted file mode 100644 index 628995aa5f07a632a0984cf2e65e6ea5f2ca419b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49224 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 diff --git a/qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png b/qdrant-landing/static/documentation/tutorials/branch-aware-search/chunk-id-mapping.png deleted file mode 100644 index 0e062678bd955ff14332f03ffb70bce08f064cfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76636 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

>%!+!lP54Rawcd~@QGaSutVNw2G6&rhX+|(l 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 From 126ff5e7094a767ede3ca1de4456fe8efb227e58 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Thu, 28 May 2026 17:00:24 -0400 Subject: [PATCH 05/10] Rewrite branch-aware search tutorial, add verifier Co-Authored-By: Claude Opus 4.8 (1M context) --- branch_aware_verifier.py | 202 ++++++ .../branch-aware-search.md | 591 ++++++++++-------- 2 files changed, 519 insertions(+), 274 deletions(-) create mode 100644 branch_aware_verifier.py diff --git a/branch_aware_verifier.py b/branch_aware_verifier.py new file mode 100644 index 000000000..90ccaed35 --- /dev/null +++ b/branch_aware_verifier.py @@ -0,0 +1,202 @@ +""" +Verifier for the Branch-Aware Search tutorial. + +Replicates the tutorial's functions verbatim (final forms) and runs the exact +seven-step fixture, asserting every commented output the tutorial commits to. + +Only difference from the tutorial, forced by running against a LOCAL Docker +Qdrant instead of Qdrant Cloud: the client points at http://localhost:6333 with +local inference (cloud_inference defaults to False), so models.Document is embedded +client-side via fastembed. Everything else (model name, operations, payload, filter +shape) matches the tutorial exactly. + +Run: .venv-verify/bin/python branch_aware_verifier.py +""" + +import uuid +from qdrant_client import QdrantClient, models + +MODEL = "sentence-transformers/all-MiniLM-L6-v2" # fastembed-canonical casing (see note above) +COLLECTION = "content" + +client = QdrantClient(url="http://localhost:6333") + +# --------------------------------------------------------------------------- +# Collection + indexes (tutorial: create_collection in Setup, indexes per step) +# --------------------------------------------------------------------------- +if client.collection_exists(COLLECTION): + client.delete_collection(COLLECTION) + +client.create_collection( + collection_name=COLLECTION, + vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE), +) + +client.create_payload_index(collection_name=COLLECTION, field_name="path", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index(collection_name=COLLECTION, field_name="branch", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index(collection_name=COLLECTION, field_name="overwritten_in.by", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index(collection_name=COLLECTION, field_name="seq", field_schema=models.PayloadSchemaType.INTEGER) +client.create_payload_index(collection_name=COLLECTION, field_name="overwritten_in.seq", field_schema=models.PayloadSchemaType.INTEGER) + +# --------------------------------------------------------------------------- +# Tutorial functions (final forms, verbatim) +# --------------------------------------------------------------------------- +NS = uuid.UUID("00000000-0000-0000-0000-000000000042") + +def point_id(branch, seq, path): + return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}")) + +def supersede(point, by, seq): + marks = point.payload["overwritten_in"] + [{"by": by, "seq": seq}] + client.set_payload(collection_name=COLLECTION, payload={"overwritten_in": marks}, points=[point.id]) + +def update(file_name, branch, seq, content): + prev = lookup(file_name, branch) + if prev: + supersede(prev, by=branch, seq=seq) + client.upsert(collection_name=COLLECTION, points=[models.PointStruct( + id=point_id(branch, seq, file_name), + vector=models.Document(text=content, model=MODEL), + payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, + )]) + +def delete(file_name, branch, seq): + supersede(lookup(file_name, branch), by=branch, seq=seq) # mark, no replacement point + +branches = {"root": None} # branch -> (parent, fork_seq), or None for the root + +def fork(child, parent, at_seq): + branches[child] = (parent, at_seq) + +def ancestry(branch): + yield (branch, None) # the branch itself: no upper bound on its own commits + node = branches[branch] + while node is not None: + parent, fork_seq = node + yield (parent, fork_seq) + node = branches[parent] + +def visibility_filter(branch, path=None): + must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] + should, must_not = [], [] + for b, cut in ancestry(branch): + candidate = [models.FieldCondition(key="branch", match=models.MatchValue(value=b))] + excluded = [models.FieldCondition(key="by", match=models.MatchValue(value=b))] + if cut is not None: # an ancestor: only up to the fork point + candidate.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + excluded.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + should.append(models.Filter(must=candidate)) + must_not.append(models.NestedCondition(nested=models.Nested( + key="overwritten_in", filter=models.Filter(must=excluded)))) + return models.Filter(must=must, should=should, must_not=must_not) + +def lookup(file_name, branch): + points, _ = client.scroll( + collection_name=COLLECTION, + scroll_filter=visibility_filter(branch, path=file_name), + limit=1, with_payload=True, + ) + return points[0] if points else None + +def search(query, branch, limit=5): + return client.query_points( + collection_name=COLLECTION, + query=models.Document(text=query, model=MODEL), + query_filter=visibility_filter(branch), + limit=limit, with_payload=True, + ).points + +# --------------------------------------------------------------------------- +# Assertion helpers +# --------------------------------------------------------------------------- +PASS = [] + +def check(label, got, want): + assert got == want, f"\nFAIL: {label}\n got: {got!r}\n want: {want!r}" + PASS.append(label) + print(f"OK {label}") + +def content(file_name, branch): + p = lookup(file_name, branch) + return p.payload["content"] if p else None + +# Exact content strings from the tutorial +PRICING_V1 = "The Pro tier costs $29 per month with 100 GB of storage." +PRICING_V2 = "The Pro tier costs $39 per month with 200 GB of storage and unlimited seats." +PRICING_A = "The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO." +PRICING_B = "The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR." +NOTES = "Internal launch notes. Remove before the public release." +TERMS_V1 = "Standard terms of service apply to all plans." +TERMS_V2 = "Updated terms of service, effective Q3, with the new data-retention clause." +ROADMAP = "Draft roadmap for the next two quarters. Not for publication." + +# --------------------------------------------------------------------------- +# Step 1: add files on root (seq 0) +# --------------------------------------------------------------------------- +update("pricing.md", "root", seq=0, content=PRICING_V1) +update("notes.md", "root", seq=0, content=NOTES) +update("terms.md", "root", seq=0, content=TERMS_V1) +check("step 1 lookup pricing.md@root == $29", content("pricing.md", "root"), PRICING_V1) + +# --------------------------------------------------------------------------- +# Step 2: update pricing.md on root (seq 1) +# --------------------------------------------------------------------------- +update("pricing.md", "root", seq=1, content=PRICING_V2) +check("step 2 lookup pricing.md@root == $39", content("pricing.md", "root"), PRICING_V2) + +# --------------------------------------------------------------------------- +# Step 3: delete notes.md on root (seq 2) +# --------------------------------------------------------------------------- +delete("notes.md", "root", seq=2) +check("step 3 lookup notes.md@root is None", content("notes.md", "root"), None) + +# --------------------------------------------------------------------------- +# Step 4: fork A at root seq 2, add roadmap.md (A seq 0) +# --------------------------------------------------------------------------- +fork("A", "root", at_seq=2) +update("roadmap.md", "A", seq=0, content=ROADMAP) +check("step 4 lookup roadmap.md@A == draft", content("roadmap.md", "A"), ROADMAP) +check("step 4 lookup roadmap.md@root is None", content("roadmap.md", "root"), None) +check("step 4 lookup pricing.md@A inherited", content("pricing.md", "A"), PRICING_V2) + +# --------------------------------------------------------------------------- +# Step 5: A updates inherited pricing.md (A seq 1) +# --------------------------------------------------------------------------- +update("pricing.md", "A", seq=1, content=PRICING_A) +check("step 5 lookup pricing.md@A == SSO", content("pricing.md", "A"), PRICING_A) +check("step 5 lookup pricing.md@root unchanged", content("pricing.md", "root"), PRICING_V2) + +# --------------------------------------------------------------------------- +# Step 6: root commits terms.md after the fork (root seq 3) +# --------------------------------------------------------------------------- +update("terms.md", "root", seq=3, content=TERMS_V2) +check("step 6 lookup terms.md@A pre-fork version", content("terms.md", "A"), TERMS_V1) +check("step 6 lookup terms.md@root updated", content("terms.md", "root"), TERMS_V2) + +# --------------------------------------------------------------------------- +# Step 7: fork B at root seq 3, overwrite pricing.md (B seq 0) +# --------------------------------------------------------------------------- +fork("B", "root", at_seq=3) +update("pricing.md", "B", seq=0, content=PRICING_B) +check("step 7 lookup pricing.md@root", content("pricing.md", "root"), PRICING_V2) +check("step 7 lookup pricing.md@A", content("pricing.md", "A"), PRICING_A) +check("step 7 lookup pricing.md@B", content("pricing.md", "B"), PRICING_B) + +# --------------------------------------------------------------------------- +# Searching a Branch (semantic, dense) +# --------------------------------------------------------------------------- +QUERY = "how much does the pro plan cost" +expected_search = {"root": PRICING_V2, "A": PRICING_A, "B": PRICING_B} +for branch, want in expected_search.items(): + top = search(QUERY, branch, limit=1)[0].payload["content"] + check(f"search {QUERY!r}@{branch} top-1", top, want) + +# --------------------------------------------------------------------------- +# Physical point count: 3 (root seq0) + 1 (root seq1) + 1 (root seq3) +# + 1 (A roadmap) + 1 (A pricing) + 1 (B pricing) = 8 +# (the notes.md delete writes no point) +# --------------------------------------------------------------------------- +stored = client.count(COLLECTION, exact=True).count +check("physical points stored == 8", stored, 8) + +print(f"\nALL ASSERTIONS PASSED ({len(PASS)} checks). Physical points stored: {stored}") diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index 34231c7c8..f6f08b3e4 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -1,7 +1,7 @@ --- title: Branch-Aware Search short_description: "Scope search to a version-controlled branch in Qdrant so a query returns that branch's live view, inherited from its ancestors." -description: "Index a versioned corpus in Qdrant and scope queries to a branch's live view with an ancestor-walk filter over per-version branch and seq payload fields." +description: "Index a versioned document corpus in Qdrant and scope each query to one branch's live view by walking its lineage with a payload filter." weight: 12 aliases: - /documentation/tutorials/branch-aware-search/ @@ -12,114 +12,21 @@ aliases: | Time: 25 min | Level: Intermediate | | --- | ----------- | -A search on a specific branch should return exactly the content that is live on that branch, and nothing from another. Without that scoping, a query returns content from other branches, or a version this branch already replaced. - -Each version of a file is one point in the Qdrant collection, tagged with the `branch` that wrote it and a `seq` (the commit number on that branch).
When a later commit overwrites or deletes a file, the previous version's point records which branch did it, and when. A search on a branch is then a single filter: it reads from that branch and the branches it forked from, and skips anything they later replaced. +When a document corpus is versioned with git-style branches, an ordinary search leaks across them, returning content from another branch or a version the current branch already replaced. This tutorial indexes such a corpus in Qdrant and scopes each query to a single branch's live view: its own commits plus what it inherited from its ancestors, and nothing a later commit replaced. The pattern fits a documentation site with draft and published branches, a policy repository with regional forks, or a codebase where each feature branch needs its own view. ## Setup -Install the Python client: - -```bash -pip install "qdrant-client>=1.18" -``` +Each version of a file is one point, embedded with a dense model for semantic search. The [Cloud Quickstart](/documentation/cloud-quickstart/) covers creating a cluster and connecting a client.

-## The Synthetic Corpus - -The corpus is a small documentation site: pricing pages, policies, getting-started guides, and API reference. There are three branches: `main` is the published version, with `pricing-refresh` (marketing's) and `compliance-update` (legal's) forked from it. - -```python -main_docs = { - "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats.", - "pricing/enterprise.md": "Enterprise pricing is customized based on volume. Contact sales for a quote, custom SLAs, and an SSO demo.", - "pricing/free-tier.md": "The Free tier is permanently free with 1 GB of storage and 100 API calls per day. No credit card required.", - "policies/refunds.md": "Refunds are processed within 30 days of the original purchase. Pro-rated charges apply for partial periods.", - "policies/data-retention.md": "Customer data is retained for the duration of the contract and 90 days after termination, then permanently deleted.", - "policies/acceptable-use.md": "Our acceptable use policy prohibits scraping, automated abuse, and any content that violates applicable laws.", - "getting-started/install.md": "Install the CLI with pip install our-tool. The default config writes to ~/.our-tool/config.json.", - "getting-started/auth.md": "Authenticate with an API key from your dashboard. Set it via OUR_TOOL_API_KEY or in the config file.", - "getting-started/first-query.md": "Run your first query with our-tool query 'your search here'. Results return as JSON or YAML.", - "guides/python-sdk.md": "The Python SDK wraps every API endpoint. Install with pip install our-tool-sdk and import from our_tool.", - "guides/typescript-sdk.md": "The TypeScript SDK ships type definitions for all API responses. Install with npm install @our-tool/sdk.", - "guides/migrations.md": "Schema migrations run automatically on deployment. Roll back with our-tool migrate --rollback.", - "guides/monitoring.md": "Metrics export in Prometheus format on port 9090. Dashboard templates ship for Grafana.", - "guides/troubleshooting.md": "Common errors and their resolutions. Check the status page first if multiple users are affected.", - "guides/best-practices.md": "Production-grade recommendations for security, performance, and reliability.", - "api/overview.md": "The REST API uses standard HTTPS with bearer token auth. All responses are JSON.", - "api/rate-limits.md": "Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits.", - "api/errors.md": "All errors return standard HTTP status codes with a JSON body describing the cause.", - "README.md": "Overview of our tool for new users. Start with the Getting Started guide.", - "CHANGELOG.md": "Release notes by version, newest first. Major releases follow semantic versioning.", -} - -# Marketing branch: raise the Pro price and refresh enterprise messaging. -pricing_refresh = { - "pricing/pro-tier.md": "The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support.", - "pricing/enterprise.md": "Enterprise pricing starts at $499 per month with custom volume discounts. SSO, audit logs, and dedicated support included.", -} - -# Compliance branch: add GDPR language to the Pro tier page and tighten the refund window. -compliance_update = { - "pricing/pro-tier.md": "The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage.", - "policies/refunds.md": "Refunds are processed within 14 days for Pro customers and 30 days for Free tier. No questions asked within the first 7 days.", -} - -# main keeps committing after the two branches fork off: it edits a shared file and adds a new one. -main_later_edit = { - "api/rate-limits.md": "Free plan is 60 requests per minute. Pro and Enterprise are unlimited within fair use, with burst credits.", - "policies/sla.md": "Service level agreement: 99.9% uptime for Pro and Enterprise, measured monthly with service credits for breaches.", -} -``` - -Two cases this design has to get right: - -1. **Per-branch versions of the same file.** Both forks edit `pricing/pro-tier.md`: `pricing-refresh` raises the price and adds SSO, while `compliance-update` adds GDPR copy at the original price. The same path should resolve to three different versions across the three branches. -2. **Timing.** After both branches fork off `main`, `main` keeps moving: it edits the shared `api/rate-limits.md` and adds `policies/sla.md`. Each fork should still see `main`'s files as they were at the fork point, not these later changes. - -## How a Branch Sees Content - -A branch records its fork point as `(parent, parent's seq at fork)`. From there, what the branch sees is its own commits plus everything inherited from its ancestors up to the fork point, minus anything superseded along the way: - -```text -main ──● seq0 ───────────────────● seq1 - │ (20 base files) (edit api/rate-limits.md, add policies/sla.md) - ├── pricing-refresh ● seq0 - └── compliance-update ● seq0 -``` - -Both branches forked at `main seq0`, so each sees `main` as it stood at that fork. - -Three pieces of runtime state encode this in plain Python: - -```python -lineage: dict[str, tuple[str, int] | None] = {} # branch -> (parent, fork_seq) -last_seq: dict[str, int] = {} # branch -> its latest commit seq (-1 before first) -head: dict[str, dict[str, str]] = {} # branch -> {path -> current point id} -``` - -`lineage` is the durable per-branch record.
`head` and `last_seq` are replay bookkeeping, rebuilt from version control on startup. - - - -## Collection Schema - -Each point in the collection has three payload fields: - -- `branch`: the branch that wrote a version. -- `seq`: a per-branch counter we assign on each commit (`0`, `1`, `2`, ...), not the git commit hash. It orders each branch's commits, so the filter can include only the commits at or before each ancestor's fork point. -- `overwritten_in`: a list of `{by, seq}` records, each meaning "replaced in branch `by` at that branch's commit `seq`." Overwrites and deletes both append one. - -Every field the visibility filter touches needs a payload index: - ```python from qdrant_client import QdrantClient, models +MODEL = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim dense vectors, free on Cloud Inference + # Replace url and api_key with your own from https://cloud.qdrant.io client = QdrantClient( url="https://xyz-example.qdrant.io:6333", @@ -129,220 +36,356 @@ client = QdrantClient( client.create_collection( collection_name="content", - sparse_vectors_config={ - "bm25": models.SparseVectorParams(modifier=models.Modifier.IDF), - }, -) - -client.create_payload_index( - collection_name="content", - field_name="branch", - field_schema=models.PayloadSchemaType.KEYWORD, -) -client.create_payload_index( - collection_name="content", - field_name="seq", - field_schema=models.PayloadSchemaType.INTEGER, -) -client.create_payload_index( - collection_name="content", - field_name="overwritten_in[].by", - field_schema=models.PayloadSchemaType.KEYWORD, -) -client.create_payload_index( - collection_name="content", - field_name="overwritten_in[].seq", - field_schema=models.PayloadSchemaType.INTEGER, + vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE), ) ``` -## Ingest - -A commit writes a new point for each file it adds or changes, tagged with the branch and its current `seq`. If the file already had a version on this branch, that prior point gets a `{by, seq}` appended to the `overwritten_in` field, marking it superseded. -
The point ID is derived from `(branch, seq, path)`, so replaying the same history produces the same IDs and the rebuild path can `upsert` without duplicating points. - - +Every point also gets a derived ID, so replaying the same history produces the same IDs and a rebuild can `upsert` without duplicating points: ```python import uuid NS = uuid.UUID("00000000-0000-0000-0000-000000000042") -# BM25's key parameter for short fields: the average word count. -word_counts = [len(text.split()) for text in main_docs.values()] -AVG_LEN = round(sum(word_counts) / len(word_counts), 1) # ~15.3 here - def point_id(branch: str, seq: int, path: str) -> str: return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}")) - -def create_branch(name: str, parent: str | None): - """Record a branch. A fork inherits the parent's view, no points written.""" - last_seq[name] = -1 - if parent is None: - lineage[name] = None - head[name] = {} - else: - # fork point: parent + parent's latest seq - lineage[name] = (parent, last_seq[parent]) - head[name] = dict(head[parent]) # start from parent's view - -def supersede(branch: str, path: str, at_seq: int): - """Mark the version of `path` that `branch` currently sees, at `at_seq`.""" - prev = head[branch].get(path) - if prev is None: - return - point = client.retrieve("content", ids=[prev])[0] - marks = point.payload["overwritten_in"] - marks.append({"by": branch, "seq": at_seq}) - client.set_payload( - "content", - payload={"overwritten_in": marks}, - points=[prev], - ) - -def commit(branch: str, writes: dict[str, str] | None = None, deletes: list[str] | None = None): - """One commit at one seq: write or overwrite files, and/or delete files.""" - writes, deletes = writes or {}, deletes or [] - seq = last_seq[branch] + 1 - last_seq[branch] = seq - points = [] - for path, content in writes.items(): - supersede(branch, path, seq) # mark the version this commit replaces - pid = point_id(branch, seq, path) - points.append(models.PointStruct( - id=pid, - vector={ - "bm25": models.Document( - text=content, - model="qdrant/bm25", - options={"avg_len": AVG_LEN}, - ), - }, - payload={ - "path": path, - "content": content, - "branch": branch, - "seq": seq, - "overwritten_in": [], - }, - )) - head[branch][path] = pid - for path in deletes: - supersede(branch, path, seq) # same record, no replacement point - head[branch].pop(path, None) - if points: - client.upsert(collection_name="content", points=points, wait=True) ``` -A delete writes the same record as an overwrite, with no replacement point behind it, and it is a real commit, so it carries a `seq` like any other. That `seq` is what lets a branch that forked before the delete keep the file, while the branch that deleted it (and anything forked after) does not. + -Build the fixture: +## Step 1: Add a File -```python -create_branch("main", parent=None) -commit("main", writes=main_docs) +Start with a single `root` branch and one commit that adds a file. -create_branch("pricing-refresh", parent="main") # forks at main seq0 -commit("pricing-refresh", writes=pricing_refresh) - -create_branch("compliance-update", parent="main") # also at main seq0 -commit( - "compliance-update", - writes=compliance_update, - deletes=["policies/acceptable-use.md"], -) - -commit("main", writes=main_later_edit) # main seq1, after both forks +```text +root ● seq 0: add pricing.md ``` -Each fork wrote only the files it changed, and `main`'s later commit wrote two points (the edited `api/rate-limits.md` and the new `policies/sla.md`). The collection holds 26 physical points: the 20 base files, two versions from each fork, and the two from `main`'s later commit. - -## Query - -A query on a branch is one filter built from a lineage walk.
For each branch on the path there's a **cutoff**, the highest `seq` to include from that branch: for the querying branch itself, that's its latest commit; for an ancestor, the `seq` at which this line forked from it.
Both filter clauses key off those cutoffs. The `should` clause (the candidates to consider) gathers each branch's versions up to its cutoff. The `must_not` clause (the ones to drop) excludes anything that branch superseded at or before its cutoff, using a [nested filter](/documentation/search/filtering/#nested-object-filter) so `by` and `seq` match on the same record: +A commit is numbered with a per-branch `seq` (`0`, `1`, `2`, ...). Each file it writes becomes a point tagged with the `branch` that wrote it, the `seq`, and the file `path`. To find a file on a branch later, we filter on `path` and `branch`, so both need a payload index: ```python -def candidate_clause(b: str, cut: int) -> models.Filter: - """Versions written on branch b with seq up to cut.""" - return models.Filter(must=[ - models.FieldCondition(key="branch", match=models.MatchValue(value=b)), - models.FieldCondition(key="seq", range=models.Range(lte=cut)), - ]) +client.create_payload_index(collection_name="content", field_name="path", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index(collection_name="content", field_name="branch", field_schema=models.PayloadSchemaType.KEYWORD) +``` -def exclusion_clause(b: str, cut: int) -> models.NestedCondition: - """Match if overwritten_in has {by: b, seq <= cut} on the same record.""" - return models.NestedCondition(nested=models.Nested( - key="overwritten_in", - filter=models.Filter(must=[ - models.FieldCondition(key="by", match=models.MatchValue(value=b)), - models.FieldCondition(key="seq", range=models.Range(lte=cut)), +The write itself embeds the content and stores the three fields, plus an empty `overwritten_in` list we'll use shortly: + +```python +def update(file_name, branch, seq, content): + client.upsert(collection_name="content", points=[models.PointStruct( + id=point_id(branch, seq, file_name), + vector=models.Document(text=content, model=MODEL), + payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, + )]) +``` + +Commit `seq 0` on `root`, which writes three files: + +```python +update("pricing.md", "root", seq=0, content="The Pro tier costs $29 per month with 100 GB of storage.") +update("notes.md", "root", seq=0, content="Internal launch notes. Remove before the public release.") +update("terms.md", "root", seq=0, content="Standard terms of service apply to all plans.") +``` + +Reading a file back on a branch is a filter on `path` and `branch`. Use `scroll`, which returns points matching a filter: + +```python +def lookup(file_name, branch): + points, _ = client.scroll( + collection_name="content", + scroll_filter=models.Filter(must=[ + models.FieldCondition(key="path", match=models.MatchValue(value=file_name)), + models.FieldCondition(key="branch", match=models.MatchValue(value=branch)), ]), + limit=1, with_payload=True, + ) + return points[0] if points else None +``` + +```python +lookup("pricing.md", "root").payload["content"] +# The Pro tier costs $29 per month with 100 GB of storage. +``` + +## Step 2: Update a File + +A second commit changes a file that already exists. + +```text +root ●───────● seq 1: update pricing.md + seq 0 seq 1 +``` + +The old point stays in the collection. We mark it superseded so a later read skips it: `overwritten_in` is a list of `{by, seq}` records, each meaning "replaced in branch `by` at that branch's commit `seq`." A write now first marks the version this branch currently sees, then upserts the new one: + +```python +def supersede(point, by, seq): + marks = point.payload["overwritten_in"] + [{"by": by, "seq": seq}] + client.set_payload(collection_name="content", payload={"overwritten_in": marks}, points=[point.id]) + +def update(file_name, branch, seq, content): + prev = lookup(file_name, branch) + if prev: + supersede(prev, by=branch, seq=seq) + client.upsert(collection_name="content", points=[models.PointStruct( + id=point_id(branch, seq, file_name), + vector=models.Document(text=content, model=MODEL), + payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, + )]) +``` + +To skip the superseded point, the read excludes any version this branch marked. That's the first index on `overwritten_in`, on the branch that did the replacing: + +```python +client.create_payload_index(collection_name="content", field_name="overwritten_in.by", field_schema=models.PayloadSchemaType.KEYWORD) +``` + +`lookup` now adds a `must_not` that drops anything carrying this branch's mark: + +```python +def visibility_filter(branch, path=None): + must = [models.FieldCondition(key="branch", match=models.MatchValue(value=branch))] + if path: + must.append(models.FieldCondition(key="path", match=models.MatchValue(value=path))) + excluded = models.NestedCondition(nested=models.Nested( + key="overwritten_in", + filter=models.Filter(must=[models.FieldCondition(key="by", match=models.MatchValue(value=branch))]), )) + return models.Filter(must=must, must_not=[excluded]) -def visibility_filter(branch: str) -> models.Filter: - # Walk lineage: (branch, cutoff) for this branch then each ancestor. - cutoffs = [(branch, last_seq[branch])] - node = lineage[branch] +def lookup(file_name, branch): + points, _ = client.scroll( + collection_name="content", + scroll_filter=visibility_filter(branch, path=file_name), + limit=1, with_payload=True, + ) + return points[0] if points else None +``` + +Every point still records its `seq`; the filter only uses it once branches appear. + +```python +update("pricing.md", "root", seq=1, content="The Pro tier costs $39 per month with 200 GB of storage and unlimited seats.") + +lookup("pricing.md", "root").payload["content"] +# The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +``` + +## Step 3: Delete a File + +A delete is a commit too, so it carries a `seq` like any other. It writes the same supersession mark as an overwrite, with no replacement point behind it: + +```text +root ●───────●───────● seq 2: delete notes.md + seq 0 seq 1 seq 2 +``` + +```python +def delete(file_name, branch, seq): + supersede(lookup(file_name, branch), by=branch, seq=seq) # mark, no replacement point +``` + +```python +delete("notes.md", "root", seq=2) + +lookup("notes.md", "root") +# None +``` + +The point is still stored, but `notes.md` no longer resolves on `root`. That recorded `seq` is what will let a branch that forked before the delete keep the file. + +## Step 4: Branch Off the Root + +A fork records where it started, the parent and the parent's latest `seq`, and writes no points. Its view begins as whatever the parent has at that `seq`. + +```text +root ●──●──● seq 0–2 + └── A ● A forks at root seq 2; A seq 0: add roadmap.md +``` + +Lineage is the only durable state, and it mirrors what your version control already tracks. Store it however you like; here it's one record per branch: + +```python +branches = {"root": None} # branch -> (parent, fork_seq), or None for the root + +def fork(child, parent, at_seq): + branches[child] = (parent, at_seq) +``` + +A read on a branch now considers that branch and every ancestor up to its fork point. Walk the lineage, then build one candidate clause and one exclusion clause per branch on the path. The candidates (a `should`, so any one can match) gather each branch's versions; the exclusions (a `must_not`) drop anything those branches superseded: + +```python +def ancestry(branch): + yield branch + node = branches[branch] while node is not None: - parent, fork = node - cutoffs.append((parent, fork)) - node = lineage[parent] + parent, _ = node + yield parent + node = branches[parent] +def visibility_filter(branch, path=None): + must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] should, must_not = [], [] - for b, cut in cutoffs: - should.append(candidate_clause(b, cut)) - must_not.append(exclusion_clause(b, cut)) - return models.Filter(should=should, must_not=must_not) + for b in ancestry(branch): + should.append(models.Filter(must=[models.FieldCondition(key="branch", match=models.MatchValue(value=b))])) + must_not.append(models.NestedCondition(nested=models.Nested( + key="overwritten_in", + filter=models.Filter(must=[models.FieldCondition(key="by", match=models.MatchValue(value=b))]), + ))) + return models.Filter(must=must, should=should, must_not=must_not) +``` -def search(branch: str, query: str, limit: int = 5): +Fork `A` and add a file that only `A` has: + +```python +fork("A", "root", at_seq=2) +update("roadmap.md", "A", seq=0, content="Draft roadmap for the next two quarters. Not for publication.") + +lookup("roadmap.md", "A").payload["content"] # A's own file +# Draft roadmap for the next two quarters. Not for publication. +lookup("roadmap.md", "root") # root never sees it +# None +lookup("pricing.md", "A").payload["content"] # inherited from root +# The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +``` + +## Step 5: Change an Inherited File on the Branch + +`A` edits `pricing.md`, a file it inherited from `root`. + +```text +root ●──●──● seq 0–2 + └── A ●──● A seq 1: update pricing.md +``` + +Nothing new is needed. `update` looks up the version `A` currently sees, which is `root`'s, and marks it, this time with `by: A`: + +```python +update("pricing.md", "A", seq=1, content="The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO.") +``` + +The mark `{by: A, seq: 1}` lands on `root`'s `pricing.md` point. Because the exclusion matches on `by`, it removes that version for `A` but not for `root`, whose filter only drops versions marked `by: root`. The same file now resolves differently on each branch: + +```python +lookup("pricing.md", "A").payload["content"] +# The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. +lookup("pricing.md", "root").payload["content"] +# The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +``` + +## Step 6: Commit on the Root After the Fork + +`root` keeps moving after `A` forked off. It updates `terms.md`, a file `A` inherited but never touched. + +```text +root ●──●──●──────● root seq 3: update terms.md + └── A ●──● A forked at seq 2, so it should not see seq 3 +``` + +```python +update("terms.md", "root", seq=3, content="Updated terms of service, effective Q3, with the new data-retention clause.") +``` + +`A` forked at `root` `seq 2`, so it should not see anything `root` committed later, including this `seq 3` edit. The bound is the `seq` we've recorded all along: each ancestor counts only up to the `seq` where this branch forked from it.
The cutoff applies on both sides of the filter: the candidate clause includes an ancestor's versions only up to the fork `seq`, and the exclusion clause honors only the marks it stamped by then, so a later change can neither appear nor hide the version `A` kept. + +That cutoff is a range on `seq`, on both the points and the marks, so both fields need an index: + +```python +client.create_payload_index(collection_name="content", field_name="seq", field_schema=models.PayloadSchemaType.INTEGER) +client.create_payload_index(collection_name="content", field_name="overwritten_in.seq", field_schema=models.PayloadSchemaType.INTEGER) +``` + +The walk now carries each branch's cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form of the filter: + +```python +def ancestry(branch): + yield (branch, None) # the branch itself: no upper bound on its own commits + node = branches[branch] + while node is not None: + parent, fork_seq = node + yield (parent, fork_seq) + node = branches[parent] + +def visibility_filter(branch, path=None): + must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] + should, must_not = [], [] + for b, cut in ancestry(branch): + candidate = [models.FieldCondition(key="branch", match=models.MatchValue(value=b))] + excluded = [models.FieldCondition(key="by", match=models.MatchValue(value=b))] + if cut is not None: # an ancestor: only up to the fork point + candidate.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + excluded.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + should.append(models.Filter(must=candidate)) + must_not.append(models.NestedCondition(nested=models.Nested( + key="overwritten_in", filter=models.Filter(must=excluded)))) + return models.Filter(must=must, should=should, must_not=must_not) +``` + +The candidate clause stops including `root`'s versions past `seq 2`, and the exclusion ignores marks stamped after `seq 2`. `A` keeps the `terms.md` it forked from; `root` sees the new one: + +```python +lookup("terms.md", "A").payload["content"] +# Standard terms of service apply to all plans. +lookup("terms.md", "root").payload["content"] +# Updated terms of service, effective Q3, with the new data-retention clause. +``` + +## Step 7: A Second Branch, Same File + +Fork `B` off `root` and have it overwrite `pricing.md`, the same file `A` already changed. + +```text +root ●──●──●──────● root seq 0–3 + ├── A ●──● A: pricing.md with SSO + └── B ● B forks at root seq 3; B seq 0: update pricing.md +``` + +```python +fork("B", "root", at_seq=3) +update("pricing.md", "B", seq=0, content="The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR.") +``` + +`root`'s `pricing.md` point now carries two marks, `{by: A, seq: 1}` and `{by: B, seq: 0}`. Each branch's exclusion matches only its own `by` on a single record, so the three branches resolve the same path three ways, with no leakage between the two forks: + +```python +for branch in ["root", "A", "B"]: + print(branch, "→", lookup("pricing.md", branch).payload["content"]) +# root → The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +# A → The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. +# B → The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. +``` + +## Searching a Branch + +`lookup` resolves one known path; a search ranks the whole corpus by relevance. It's the same visibility filter, passed to a semantic query instead of a path scroll: + +```python +def search(query, branch, limit=5): return client.query_points( collection_name="content", - query=models.Document( - text=query, - model="qdrant/bm25", - options={"avg_len": AVG_LEN}, - ), - using="bm25", + query=models.Document(text=query, model=MODEL), query_filter=visibility_filter(branch), - limit=limit, - with_payload=True, + limit=limit, with_payload=True, ).points ``` -Run a query that lands on `pricing/pro-tier.md`, against all three branches: +The same query, scoped to each branch, returns that branch's live version of the page: ```python -for branch in ["main", "pricing-refresh", "compliance-update"]: - print(f"{branch}:", search(branch, "Pro tier storage and seats", limit=1)[0].payload["content"]) - -# Output: -# main: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. -# pricing-refresh: The Pro tier costs $39 per month and includes 200 GB of storage, unlimited API calls, team collaboration for unlimited seats, and SSO support. -# compliance-update: The Pro tier costs $29 per month and includes 100 GB of storage, unlimited API calls, and team collaboration for up to 10 seats. EU customers: data is processed under GDPR with EU-region storage. +for branch in ["root", "A", "B"]: + print(branch, "→", search("how much does the pro plan cost", branch, limit=1)[0].payload["content"]) +# root → The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +# A → The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. +# B → The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. ``` -Same query, three branches, three different answers. `pricing-refresh` returns its $39 page with SSO. `compliance-update` returns its GDPR variant. `main` returns the original. - -Now the timing case. `main` edited `api/rate-limits.md` at `seq 1`, after both branches forked. `main` now shows the new text; the forks still see the version they inherited: - -```python -for branch in ["main", "pricing-refresh"]: - print(f"{branch}:", search(branch, "rate limits requests free plan", limit=1)[0].payload["content"]) - -# Output: -# main: Free plan is 60 requests per minute. Pro and Enterprise are unlimited within fair use, with burst credits. -# pricing-refresh: Free plan is 100 requests per day. Pro plan is unlimited within fair use. Enterprise has custom limits. -``` - -`pricing-refresh` forked at `main`'s `seq 0`, before the `seq 1` edit. Its cutoff for `main` is `0`, so the record stamped at `seq 1` falls past the cutoff, the exclusion doesn't apply, and the pre-edit version stays visible. The same cutoff hides `policies/sla.md` (which `main` added at `seq 1`) from both forks. - ## Scaling to Production -- **The filter grows with lineage depth, not corpus size.** The query walks the lineage once, adding one `should` clause and one `must_not` clause per ancestor. The filter's size tracks how deep a branch sits, not how many files or versions the collection holds. +- **The filter grows with lineage depth, not corpus size.** Each query walks the lineage once, adding one candidate clause and one exclusion clause per ancestor. The filter's size tracks how deep a branch sits, not how many files or versions the collection holds. -- **Qdrant is a derived index.** Branch lineage and history live in version control. Build the collection by replaying that history, and if the index ever drifts, rebuild it. Nothing here needs the index to be the source of truth. +- **Qdrant is a derived index.** Branch lineage and history live in version control. Build the collection by replaying that history, and if the index ever drifts, rebuild it. The derived point IDs make a full replay idempotent: the same commit writes the same point, so `upsert` overwrites cleanly. - **Storage grows with edits.** Every overwrite keeps the old version as a point, so long-lived branches accumulate versions. Reclaiming superseded versions is a production concern. @@ -350,6 +393,6 @@ for branch in ["main", "pricing-refresh"]: ## Related Reading -- [Filtering](/documentation/search/filtering/) for the full set of filter clauses. -- [Text Search](/documentation/search/text-search/) for BM25 and the `avg_len` parameter. +- [Filtering](/documentation/search/filtering/) for the full set of filter clauses, including the [nested object filter](/documentation/search/filtering/#nested-object-filter) that binds `by` and `seq` on the same record. - [Payload Indexing](/documentation/manage-data/indexing/#payload-index) for the index types the visibility fields use. +- [Qdrant Cloud Inference](/documentation/inference/#qdrant-cloud-inference) for generating the dense embeddings server-side. From de29ef7565834d4d1e17ed387e6826bb2aac68a7 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Thu, 28 May 2026 18:02:13 -0400 Subject: [PATCH 06/10] Wrap tutorial code blocks to avoid horizontal scroll Co-Authored-By: Claude Opus 4.8 (1M context) --- .../branch-aware-search.md | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index f6f08b3e4..f1b08a357 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -25,7 +25,8 @@ This tutorial uses Qd ```python from qdrant_client import QdrantClient, models -MODEL = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim dense vectors, free on Cloud Inference +# 384-dim dense vectors, free on Cloud Inference +MODEL = "sentence-transformers/all-MiniLM-L6-v2" # Replace url and api_key with your own from https://cloud.qdrant.io client = QdrantClient( @@ -36,7 +37,8 @@ client = QdrantClient( client.create_collection( collection_name="content", - vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE), + vectors_config=models.VectorParams( + size=384, distance=models.Distance.COSINE), ) ``` @@ -66,8 +68,14 @@ root ● seq 0: add pricing.md A commit is numbered with a per-branch `seq` (`0`, `1`, `2`, ...). Each file it writes becomes a point tagged with the `branch` that wrote it, the `seq`, and the file `path`. To find a file on a branch later, we filter on `path` and `branch`, so both need a payload index: ```python -client.create_payload_index(collection_name="content", field_name="path", field_schema=models.PayloadSchemaType.KEYWORD) -client.create_payload_index(collection_name="content", field_name="branch", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index( + collection_name="content", field_name="path", + field_schema=models.PayloadSchemaType.KEYWORD, +) +client.create_payload_index( + collection_name="content", field_name="branch", + field_schema=models.PayloadSchemaType.KEYWORD, +) ``` The write itself embeds the content and stores the three fields, plus an empty `overwritten_in` list we'll use shortly: @@ -77,7 +85,10 @@ def update(file_name, branch, seq, content): client.upsert(collection_name="content", points=[models.PointStruct( id=point_id(branch, seq, file_name), vector=models.Document(text=content, model=MODEL), - payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, + payload={ + "path": file_name, "content": content, + "branch": branch, "seq": seq, "overwritten_in": [], + }, )]) ``` @@ -96,8 +107,10 @@ def lookup(file_name, branch): points, _ = client.scroll( collection_name="content", scroll_filter=models.Filter(must=[ - models.FieldCondition(key="path", match=models.MatchValue(value=file_name)), - models.FieldCondition(key="branch", match=models.MatchValue(value=branch)), + models.FieldCondition( + key="path", match=models.MatchValue(value=file_name)), + models.FieldCondition( + key="branch", match=models.MatchValue(value=branch)), ]), limit=1, with_payload=True, ) @@ -123,7 +136,11 @@ The old point stays in the collection. We mark it superseded so a later read ski ```python def supersede(point, by, seq): marks = point.payload["overwritten_in"] + [{"by": by, "seq": seq}] - client.set_payload(collection_name="content", payload={"overwritten_in": marks}, points=[point.id]) + client.set_payload( + collection_name="content", + payload={"overwritten_in": marks}, + points=[point.id], + ) def update(file_name, branch, seq, content): prev = lookup(file_name, branch) @@ -132,26 +149,35 @@ def update(file_name, branch, seq, content): client.upsert(collection_name="content", points=[models.PointStruct( id=point_id(branch, seq, file_name), vector=models.Document(text=content, model=MODEL), - payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, + payload={ + "path": file_name, "content": content, + "branch": branch, "seq": seq, "overwritten_in": [], + }, )]) ``` To skip the superseded point, the read excludes any version this branch marked. That's the first index on `overwritten_in`, on the branch that did the replacing: ```python -client.create_payload_index(collection_name="content", field_name="overwritten_in.by", field_schema=models.PayloadSchemaType.KEYWORD) +client.create_payload_index( + collection_name="content", field_name="overwritten_in.by", + field_schema=models.PayloadSchemaType.KEYWORD, +) ``` `lookup` now adds a `must_not` that drops anything carrying this branch's mark: ```python def visibility_filter(branch, path=None): - must = [models.FieldCondition(key="branch", match=models.MatchValue(value=branch))] + must = [models.FieldCondition( + key="branch", match=models.MatchValue(value=branch))] if path: - must.append(models.FieldCondition(key="path", match=models.MatchValue(value=path))) + must.append(models.FieldCondition( + key="path", match=models.MatchValue(value=path))) excluded = models.NestedCondition(nested=models.Nested( key="overwritten_in", - filter=models.Filter(must=[models.FieldCondition(key="by", match=models.MatchValue(value=branch))]), + filter=models.Filter(must=[models.FieldCondition( + key="by", match=models.MatchValue(value=branch))]), )) return models.Filter(must=must, must_not=[excluded]) @@ -184,7 +210,7 @@ root ●───────●───────● seq 2: delete notes ```python def delete(file_name, branch, seq): - supersede(lookup(file_name, branch), by=branch, seq=seq) # mark, no replacement point + supersede(lookup(file_name, branch), by=branch, seq=seq) # no new point ``` ```python @@ -208,7 +234,7 @@ root ●──●──● seq 0–2 Lineage is the only durable state, and it mirrors what your version control already tracks. Store it however you like; here it's one record per branch: ```python -branches = {"root": None} # branch -> (parent, fork_seq), or None for the root +branches = {"root": None} # branch -> (parent, fork_seq), None for root def fork(child, parent, at_seq): branches[child] = (parent, at_seq) @@ -226,13 +252,18 @@ def ancestry(branch): node = branches[parent] def visibility_filter(branch, path=None): - must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] + must = [] + if path: + must.append(models.FieldCondition( + key="path", match=models.MatchValue(value=path))) should, must_not = [], [] for b in ancestry(branch): - should.append(models.Filter(must=[models.FieldCondition(key="branch", match=models.MatchValue(value=b))])) + should.append(models.Filter(must=[models.FieldCondition( + key="branch", match=models.MatchValue(value=b))])) must_not.append(models.NestedCondition(nested=models.Nested( key="overwritten_in", - filter=models.Filter(must=[models.FieldCondition(key="by", match=models.MatchValue(value=b))]), + filter=models.Filter(must=[models.FieldCondition( + key="by", match=models.MatchValue(value=b))]), ))) return models.Filter(must=must, should=should, must_not=must_not) ``` @@ -293,15 +324,21 @@ update("terms.md", "root", seq=3, content="Updated terms of service, effective Q That cutoff is a range on `seq`, on both the points and the marks, so both fields need an index: ```python -client.create_payload_index(collection_name="content", field_name="seq", field_schema=models.PayloadSchemaType.INTEGER) -client.create_payload_index(collection_name="content", field_name="overwritten_in.seq", field_schema=models.PayloadSchemaType.INTEGER) +client.create_payload_index( + collection_name="content", field_name="seq", + field_schema=models.PayloadSchemaType.INTEGER, +) +client.create_payload_index( + collection_name="content", field_name="overwritten_in.seq", + field_schema=models.PayloadSchemaType.INTEGER, +) ``` The walk now carries each branch's cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form of the filter: ```python def ancestry(branch): - yield (branch, None) # the branch itself: no upper bound on its own commits + yield (branch, None) # the branch itself: all its own commits node = branches[branch] while node is not None: parent, fork_seq = node @@ -309,14 +346,21 @@ def ancestry(branch): node = branches[parent] def visibility_filter(branch, path=None): - must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] + must = [] + if path: + must.append(models.FieldCondition( + key="path", match=models.MatchValue(value=path))) should, must_not = [], [] for b, cut in ancestry(branch): - candidate = [models.FieldCondition(key="branch", match=models.MatchValue(value=b))] - excluded = [models.FieldCondition(key="by", match=models.MatchValue(value=b))] + candidate = [models.FieldCondition( + key="branch", match=models.MatchValue(value=b))] + excluded = [models.FieldCondition( + key="by", match=models.MatchValue(value=b))] if cut is not None: # an ancestor: only up to the fork point - candidate.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) - excluded.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + candidate.append(models.FieldCondition( + key="seq", range=models.Range(lte=cut))) + excluded.append(models.FieldCondition( + key="seq", range=models.Range(lte=cut))) should.append(models.Filter(must=candidate)) must_not.append(models.NestedCondition(nested=models.Nested( key="overwritten_in", filter=models.Filter(must=excluded)))) @@ -375,7 +419,8 @@ The same query, scoped to each branch, returns that branch's live version of the ```python for branch in ["root", "A", "B"]: - print(branch, "→", search("how much does the pro plan cost", branch, limit=1)[0].payload["content"]) + hit = search("how much does the pro plan cost", branch, limit=1)[0] + print(branch, "→", hit.payload["content"]) # root → The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. # A → The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. # B → The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. From a66a84f6bf83cb9cf128795430365fd5e2155df8 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Thu, 28 May 2026 19:02:32 -0400 Subject: [PATCH 07/10] Restructure branch-aware tutorial, fix nested-array index notation Co-Authored-By: Claude Opus 4.8 (1M context) --- branch_aware_verifier.py | 223 +++++++++--------- .../branch-aware-search.md | 191 ++++++++------- 2 files changed, 214 insertions(+), 200 deletions(-) diff --git a/branch_aware_verifier.py b/branch_aware_verifier.py index 90ccaed35..0732f89b2 100644 --- a/branch_aware_verifier.py +++ b/branch_aware_verifier.py @@ -1,14 +1,16 @@ """ Verifier for the Branch-Aware Search tutorial. -Replicates the tutorial's functions verbatim (final forms) and runs the exact -seven-step fixture, asserting every commented output the tutorial commits to. +Replicates the tutorial's final functions and runs the seven-step fixture, +asserting every commented output. Runs in STRICT MODE (as Qdrant Cloud does), so +it actually exercises the payload-index requirements: the nested-array indexes +must be named overwritten_in[].by / overwritten_in[].seq or strict mode rejects +the filter. -Only difference from the tutorial, forced by running against a LOCAL Docker -Qdrant instead of Qdrant Cloud: the client points at http://localhost:6333 with -local inference (cloud_inference defaults to False), so models.Document is embedded -client-side via fastembed. Everything else (model name, operations, payload, filter -shape) matches the tutorial exactly. +Only difference from the tutorial: the client points at a LOCAL Docker Qdrant +with local inference (cloud_inference defaults to False), so models.Document is +embedded client-side via fastembed. Operations, payload, filter, model, and index +names match the tutorial exactly. Run: .venv-verify/bin/python branch_aware_verifier.py """ @@ -16,13 +18,14 @@ Run: .venv-verify/bin/python branch_aware_verifier.py import uuid from qdrant_client import QdrantClient, models -MODEL = "sentence-transformers/all-MiniLM-L6-v2" # fastembed-canonical casing (see note above) +MODEL = "sentence-transformers/all-MiniLM-L6-v2" COLLECTION = "content" client = QdrantClient(url="http://localhost:6333") # --------------------------------------------------------------------------- -# Collection + indexes (tutorial: create_collection in Setup, indexes per step) +# Collection in strict mode + payload indexes (bracket notation for the +# nested-array fields, which strict mode requires). # --------------------------------------------------------------------------- if client.collection_exists(COLLECTION): client.delete_collection(COLLECTION) @@ -30,16 +33,23 @@ if client.collection_exists(COLLECTION): client.create_collection( collection_name=COLLECTION, vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE), + strict_mode_config=models.StrictModeConfig( + enabled=True, unindexed_filtering_retrieve=False), ) -client.create_payload_index(collection_name=COLLECTION, field_name="path", field_schema=models.PayloadSchemaType.KEYWORD) -client.create_payload_index(collection_name=COLLECTION, field_name="branch", field_schema=models.PayloadSchemaType.KEYWORD) -client.create_payload_index(collection_name=COLLECTION, field_name="overwritten_in.by", field_schema=models.PayloadSchemaType.KEYWORD) -client.create_payload_index(collection_name=COLLECTION, field_name="seq", field_schema=models.PayloadSchemaType.INTEGER) -client.create_payload_index(collection_name=COLLECTION, field_name="overwritten_in.seq", field_schema=models.PayloadSchemaType.INTEGER) +for field_name, schema in [ + ("path", models.PayloadSchemaType.KEYWORD), + ("branch", models.PayloadSchemaType.KEYWORD), + ("seq", models.PayloadSchemaType.INTEGER), + ("overwritten_in[].by", models.PayloadSchemaType.KEYWORD), + ("overwritten_in[].seq", models.PayloadSchemaType.INTEGER), +]: + client.create_payload_index( + collection_name=COLLECTION, field_name=field_name, field_schema=schema) # --------------------------------------------------------------------------- -# Tutorial functions (final forms, verbatim) +# Tutorial functions (final forms). Ancestry is passed in: a list of +# (branch, cutoff) from the branch itself (cutoff None) up to the root. # --------------------------------------------------------------------------- NS = uuid.UUID("00000000-0000-0000-0000-000000000042") @@ -47,67 +57,66 @@ def point_id(branch, seq, path): return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}")) def supersede(point, by, seq): - marks = point.payload["overwritten_in"] + [{"by": by, "seq": seq}] - client.set_payload(collection_name=COLLECTION, payload={"overwritten_in": marks}, points=[point.id]) + marks = point.payload.get("overwritten_in", []) + [{"by": by, "seq": seq}] + client.set_payload( + collection_name=COLLECTION, + payload={"overwritten_in": marks}, + points=[point.id], + ) -def update(file_name, branch, seq, content): - prev = lookup(file_name, branch) - if prev: - supersede(prev, by=branch, seq=seq) - client.upsert(collection_name=COLLECTION, points=[models.PointStruct( - id=point_id(branch, seq, file_name), - vector=models.Document(text=content, model=MODEL), - payload={"path": file_name, "content": content, "branch": branch, "seq": seq, "overwritten_in": []}, - )]) - -def delete(file_name, branch, seq): - supersede(lookup(file_name, branch), by=branch, seq=seq) # mark, no replacement point - -branches = {"root": None} # branch -> (parent, fork_seq), or None for the root - -def fork(child, parent, at_seq): - branches[child] = (parent, at_seq) - -def ancestry(branch): - yield (branch, None) # the branch itself: no upper bound on its own commits - node = branches[branch] - while node is not None: - parent, fork_seq = node - yield (parent, fork_seq) - node = branches[parent] - -def visibility_filter(branch, path=None): - must = [models.FieldCondition(key="path", match=models.MatchValue(value=path))] if path else [] +def visibility_filter(ancestry, path=None): + must = [] + if path: + must.append(models.FieldCondition( + key="path", match=models.MatchValue(value=path))) should, must_not = [], [] - for b, cut in ancestry(branch): - candidate = [models.FieldCondition(key="branch", match=models.MatchValue(value=b))] - excluded = [models.FieldCondition(key="by", match=models.MatchValue(value=b))] + for branch, cut in ancestry: + candidate = [models.FieldCondition( + key="branch", match=models.MatchValue(value=branch))] + excluded = [models.FieldCondition( + key="by", match=models.MatchValue(value=branch))] if cut is not None: # an ancestor: only up to the fork point - candidate.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) - excluded.append(models.FieldCondition(key="seq", range=models.Range(lte=cut))) + candidate.append(models.FieldCondition( + key="seq", range=models.Range(lte=cut))) + excluded.append(models.FieldCondition( + key="seq", range=models.Range(lte=cut))) should.append(models.Filter(must=candidate)) must_not.append(models.NestedCondition(nested=models.Nested( key="overwritten_in", filter=models.Filter(must=excluded)))) return models.Filter(must=must, should=should, must_not=must_not) -def lookup(file_name, branch): +def lookup(file_name, ancestry): points, _ = client.scroll( collection_name=COLLECTION, - scroll_filter=visibility_filter(branch, path=file_name), + scroll_filter=visibility_filter(ancestry, path=file_name), limit=1, with_payload=True, ) return points[0] if points else None -def search(query, branch, limit=5): +def update(file_name, branch, seq, content, ancestry): + prev = lookup(file_name, ancestry) + if prev: + supersede(prev, by=branch, seq=seq) + client.upsert(collection_name=COLLECTION, points=[models.PointStruct( + id=point_id(branch, seq, file_name), + vector=models.Document(text=content, model=MODEL), + payload={"path": file_name, "content": content, + "branch": branch, "seq": seq, "overwritten_in": []}, + )]) + +def delete(file_name, branch, seq, ancestry): + supersede(lookup(file_name, ancestry), by=branch, seq=seq) + +def search(query, ancestry, limit=5): return client.query_points( collection_name=COLLECTION, query=models.Document(text=query, model=MODEL), - query_filter=visibility_filter(branch), + query_filter=visibility_filter(ancestry), limit=limit, with_payload=True, ).points # --------------------------------------------------------------------------- -# Assertion helpers +# Assertions # --------------------------------------------------------------------------- PASS = [] @@ -116,11 +125,10 @@ def check(label, got, want): PASS.append(label) print(f"OK {label}") -def content(file_name, branch): - p = lookup(file_name, branch) +def content(file_name, ancestry): + p = lookup(file_name, ancestry) return p.payload["content"] if p else None -# Exact content strings from the tutorial PRICING_V1 = "The Pro tier costs $29 per month with 100 GB of storage." PRICING_V2 = "The Pro tier costs $39 per month with 200 GB of storage and unlimited seats." PRICING_A = "The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO." @@ -130,72 +138,65 @@ TERMS_V1 = "Standard terms of service apply to all plans." TERMS_V2 = "Updated terms of service, effective Q3, with the new data-retention clause." ROADMAP = "Draft roadmap for the next two quarters. Not for publication." -# --------------------------------------------------------------------------- +ROOT = [("root", None)] +A = [("A", None), ("root", 2)] +B = [("B", None), ("root", 3)] + # Step 1: add files on root (seq 0) -# --------------------------------------------------------------------------- -update("pricing.md", "root", seq=0, content=PRICING_V1) -update("notes.md", "root", seq=0, content=NOTES) -update("terms.md", "root", seq=0, content=TERMS_V1) -check("step 1 lookup pricing.md@root == $29", content("pricing.md", "root"), PRICING_V1) +update("pricing.md", "root", seq=0, content=PRICING_V1, ancestry=ROOT) +update("notes.md", "root", seq=0, content=NOTES, ancestry=ROOT) +update("terms.md", "root", seq=0, content=TERMS_V1, ancestry=ROOT) +check("step 1 pricing.md@root == $29", content("pricing.md", ROOT), PRICING_V1) -# --------------------------------------------------------------------------- -# Step 2: update pricing.md on root (seq 1) -# --------------------------------------------------------------------------- -update("pricing.md", "root", seq=1, content=PRICING_V2) -check("step 2 lookup pricing.md@root == $39", content("pricing.md", "root"), PRICING_V2) +# Step 2: update pricing.md (seq 1) +update("pricing.md", "root", seq=1, content=PRICING_V2, ancestry=ROOT) +check("step 2 pricing.md@root == $39", content("pricing.md", ROOT), PRICING_V2) -# --------------------------------------------------------------------------- -# Step 3: delete notes.md on root (seq 2) -# --------------------------------------------------------------------------- -delete("notes.md", "root", seq=2) -check("step 3 lookup notes.md@root is None", content("notes.md", "root"), None) +# Step 3: delete notes.md (seq 2) +delete("notes.md", "root", seq=2, ancestry=ROOT) +check("step 3 notes.md@root is None", content("notes.md", ROOT), None) -# --------------------------------------------------------------------------- # Step 4: fork A at root seq 2, add roadmap.md (A seq 0) -# --------------------------------------------------------------------------- -fork("A", "root", at_seq=2) -update("roadmap.md", "A", seq=0, content=ROADMAP) -check("step 4 lookup roadmap.md@A == draft", content("roadmap.md", "A"), ROADMAP) -check("step 4 lookup roadmap.md@root is None", content("roadmap.md", "root"), None) -check("step 4 lookup pricing.md@A inherited", content("pricing.md", "A"), PRICING_V2) +update("roadmap.md", "A", seq=0, content=ROADMAP, ancestry=A) +check("step 4 roadmap.md@A == draft", content("roadmap.md", A), ROADMAP) +check("step 4 roadmap.md@root is None", content("roadmap.md", ROOT), None) +check("step 4 pricing.md@A inherited", content("pricing.md", A), PRICING_V2) -# --------------------------------------------------------------------------- # Step 5: A updates inherited pricing.md (A seq 1) -# --------------------------------------------------------------------------- -update("pricing.md", "A", seq=1, content=PRICING_A) -check("step 5 lookup pricing.md@A == SSO", content("pricing.md", "A"), PRICING_A) -check("step 5 lookup pricing.md@root unchanged", content("pricing.md", "root"), PRICING_V2) +update("pricing.md", "A", seq=1, content=PRICING_A, ancestry=A) +check("step 5 pricing.md@A == SSO", content("pricing.md", A), PRICING_A) +check("step 5 pricing.md@root unchanged", content("pricing.md", ROOT), PRICING_V2) -# --------------------------------------------------------------------------- -# Step 6: root commits terms.md after the fork (root seq 3) -# --------------------------------------------------------------------------- -update("terms.md", "root", seq=3, content=TERMS_V2) -check("step 6 lookup terms.md@A pre-fork version", content("terms.md", "A"), TERMS_V1) -check("step 6 lookup terms.md@root updated", content("terms.md", "root"), TERMS_V2) +# Step 6: root updates terms.md after the fork (seq 3) +update("terms.md", "root", seq=3, content=TERMS_V2, ancestry=ROOT) +check("step 6 terms.md@A pre-fork", content("terms.md", A), TERMS_V1) +check("step 6 terms.md@root new", content("terms.md", ROOT), TERMS_V2) -# --------------------------------------------------------------------------- # Step 7: fork B at root seq 3, overwrite pricing.md (B seq 0) -# --------------------------------------------------------------------------- -fork("B", "root", at_seq=3) -update("pricing.md", "B", seq=0, content=PRICING_B) -check("step 7 lookup pricing.md@root", content("pricing.md", "root"), PRICING_V2) -check("step 7 lookup pricing.md@A", content("pricing.md", "A"), PRICING_A) -check("step 7 lookup pricing.md@B", content("pricing.md", "B"), PRICING_B) +update("pricing.md", "B", seq=0, content=PRICING_B, ancestry=B) +check("step 7 pricing.md@root", content("pricing.md", ROOT), PRICING_V2) +check("step 7 pricing.md@A", content("pricing.md", A), PRICING_A) +check("step 7 pricing.md@B", content("pricing.md", B), PRICING_B) -# --------------------------------------------------------------------------- -# Searching a Branch (semantic, dense) -# --------------------------------------------------------------------------- +# Searching a branch (dense, same filter) QUERY = "how much does the pro plan cost" -expected_search = {"root": PRICING_V2, "A": PRICING_A, "B": PRICING_B} -for branch, want in expected_search.items(): - top = search(QUERY, branch, limit=1)[0].payload["content"] - check(f"search {QUERY!r}@{branch} top-1", top, want) +for anc, want, name in [(ROOT, PRICING_V2, "root"), (A, PRICING_A, "A"), (B, PRICING_B, "B")]: + top = search(QUERY, anc, limit=1)[0].payload["content"] + check(f"search cost@{name} top-1", top, want) -# --------------------------------------------------------------------------- -# Physical point count: 3 (root seq0) + 1 (root seq1) + 1 (root seq3) -# + 1 (A roadmap) + 1 (A pricing) + 1 (B pricing) = 8 -# (the notes.md delete writes no point) -# --------------------------------------------------------------------------- +# Search must not leak a hidden version even below rank 1 +for anc, hidden, name in [(A, PRICING_B, "A"), (B, PRICING_A, "B"), (ROOT, PRICING_A, "root")]: + seen = [p.payload["content"] for p in search(QUERY, anc, limit=5)] + check(f"search no leak of other branch @{name}", hidden in seen, False) + +# Step 7 leaves both marks on root's pricing.md point +root_pricing = lookup("pricing.md", ROOT) +marks = {(m["by"], m["seq"]) for m in lookup("pricing.md", ROOT) and + client.retrieve(COLLECTION, ids=[point_id("root", 1, "pricing.md")])[0].payload["overwritten_in"]} +check("marks on root pricing seq1 == {(A,1),(B,0)}", marks, {("A", 1), ("B", 0)}) + +# Physical points: 3 (root seq0) + 1 (root seq1) + 1 (root seq3) + 1 (A roadmap) +# + 1 (A pricing) + 1 (B pricing) = 8 (delete writes no point) stored = client.count(COLLECTION, exact=True).count check("physical points stored == 8", stored, 8) diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index f1b08a357..697420d21 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -78,17 +78,15 @@ client.create_payload_index( ) ``` -The write itself embeds the content and stores the three fields, plus an empty `overwritten_in` list we'll use shortly: +The write embeds the content and stores the three fields: ```python def update(file_name, branch, seq, content): client.upsert(collection_name="content", points=[models.PointStruct( id=point_id(branch, seq, file_name), vector=models.Document(text=content, model=MODEL), - payload={ - "path": file_name, "content": content, - "branch": branch, "seq": seq, "overwritten_in": [], - }, + payload={"path": file_name, "content": content, + "branch": branch, "seq": seq}, )]) ``` @@ -115,9 +113,7 @@ def lookup(file_name, branch): limit=1, with_payload=True, ) return points[0] if points else None -``` -```python lookup("pricing.md", "root").payload["content"] # The Pro tier costs $29 per month with 100 GB of storage. ``` @@ -131,11 +127,11 @@ root ●───────● seq 1: update pricing.md seq 0 seq 1 ``` -The old point stays in the collection. We mark it superseded so a later read skips it: `overwritten_in` is a list of `{by, seq}` records, each meaning "replaced in branch `by` at that branch's commit `seq`." A write now first marks the version this branch currently sees, then upserts the new one: +The old point stays in the collection. We tag it as superseded so a later read skips it, with an `overwritten_in` entry: a `{by, seq}` record naming the branch that replaced it and the commit that did so. A write now tags the version this branch currently sees, then upserts the new one with an empty `overwritten_in`: ```python def supersede(point, by, seq): - marks = point.payload["overwritten_in"] + [{"by": by, "seq": seq}] + marks = point.payload.get("overwritten_in", []) + [{"by": by, "seq": seq}] client.set_payload( collection_name="content", payload={"overwritten_in": marks}, @@ -149,23 +145,21 @@ def update(file_name, branch, seq, content): client.upsert(collection_name="content", points=[models.PointStruct( id=point_id(branch, seq, file_name), vector=models.Document(text=content, model=MODEL), - payload={ - "path": file_name, "content": content, - "branch": branch, "seq": seq, "overwritten_in": [], - }, + payload={"path": file_name, "content": content, "branch": branch, + "seq": seq, "overwritten_in": []}, )]) ``` -To skip the superseded point, the read excludes any version this branch marked. That's the first index on `overwritten_in`, on the branch that did the replacing: +To skip a superseded version, the read drops anything this branch marked. That filters on `by` inside the `overwritten_in` array, so it needs an index on that nested field, written with `[]`: ```python client.create_payload_index( - collection_name="content", field_name="overwritten_in.by", + collection_name="content", field_name="overwritten_in[].by", field_schema=models.PayloadSchemaType.KEYWORD, ) ``` -`lookup` now adds a `must_not` that drops anything carrying this branch's mark: +`lookup` now adds a `must_not` with a [nested filter](/documentation/search/filtering/#nested-object-filter) that drops any version carrying this branch's mark: ```python def visibility_filter(branch, path=None): @@ -174,12 +168,12 @@ def visibility_filter(branch, path=None): if path: must.append(models.FieldCondition( key="path", match=models.MatchValue(value=path))) - excluded = models.NestedCondition(nested=models.Nested( + replaced = models.NestedCondition(nested=models.Nested( key="overwritten_in", filter=models.Filter(must=[models.FieldCondition( key="by", match=models.MatchValue(value=branch))]), )) - return models.Filter(must=must, must_not=[excluded]) + return models.Filter(must=must, must_not=[replaced]) def lookup(file_name, branch): points, _ = client.scroll( @@ -190,7 +184,7 @@ def lookup(file_name, branch): return points[0] if points else None ``` -Every point still records its `seq`; the filter only uses it once branches appear. +`by` is always `root` for now; it starts telling branches apart once we fork. The commit: ```python update("pricing.md", "root", seq=1, content="The Pro tier costs $39 per month with 200 GB of storage and unlimited seats.") @@ -199,21 +193,32 @@ lookup("pricing.md", "root").payload["content"] # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. ``` +Both versions are still stored. The old one now carries the mark; the new one is clean: + +```python +for p in client.retrieve("content", ids=[ + point_id("root", 0, "pricing.md"), + point_id("root", 1, "pricing.md")]): + print(p.payload["branch"], p.payload["seq"], p.payload["overwritten_in"]) +# root 0 [{'by': 'root', 'seq': 1}] +# root 1 [] +``` + ## Step 3: Delete a File -A delete is a commit too, so it carries a `seq` like any other. It writes the same supersession mark as an overwrite, with no replacement point behind it: +A delete is a commit too, so it carries a `seq` like any other. ```text root ●───────●───────● seq 2: delete notes.md seq 0 seq 1 seq 2 ``` +It writes the same mark as an overwrite, with no replacement point behind it: + ```python def delete(file_name, branch, seq): supersede(lookup(file_name, branch), by=branch, seq=seq) # no new point -``` -```python delete("notes.md", "root", seq=2) lookup("notes.md", "root") @@ -224,61 +229,75 @@ The point is still stored, but `notes.md` no longer resolves on `root`. That rec ## Step 4: Branch Off the Root -A fork records where it started, the parent and the parent's latest `seq`, and writes no points. Its view begins as whatever the parent has at that `seq`. +A fork records where it started: the parent and the parent's latest `seq`. It writes no points; its view begins as whatever the parent has at that `seq`. ```text root ●──●──● seq 0–2 └── A ● A forks at root seq 2; A seq 0: add roadmap.md ``` -Lineage is the only durable state, and it mirrors what your version control already tracks. Store it however you like; here it's one record per branch: +A read on a branch now considers that branch and every ancestor up to its fork point. We pass that chain in as the branch's **ancestry**: `(branch, fork seq)` from the branch itself down to the root. Build it once when the branch is created and store it alongside the branch: ```python -branches = {"root": None} # branch -> (parent, fork_seq), None for root - -def fork(child, parent, at_seq): - branches[child] = (parent, at_seq) +# None means no cutoff (the branch's own commits); an ancestor is capped +# at its fork seq. The fork seq starts mattering in Step 6. +root_ancestry = [("root", None)] +A_ancestry = [("A", None), ("root", 2)] ``` -A read on a branch now considers that branch and every ancestor up to its fork point. Walk the lineage, then build one candidate clause and one exclusion clause per branch on the path. The candidates (a `should`, so any one can match) gather each branch's versions; the exclusions (a `must_not`) drop anything those branches superseded: +`visibility_filter` walks that ancestry, building one candidate clause and one exclusion per branch. The candidates (a `should`, so any one can match) gather each branch's versions; the exclusions (a `must_not`) drop anything that branch replaced: ```python -def ancestry(branch): - yield branch - node = branches[branch] - while node is not None: - parent, _ = node - yield parent - node = branches[parent] - -def visibility_filter(branch, path=None): +def visibility_filter(ancestry, path=None): must = [] if path: must.append(models.FieldCondition( key="path", match=models.MatchValue(value=path))) should, must_not = [], [] - for b in ancestry(branch): + for branch, _ in ancestry: should.append(models.Filter(must=[models.FieldCondition( - key="branch", match=models.MatchValue(value=b))])) + key="branch", match=models.MatchValue(value=branch))])) must_not.append(models.NestedCondition(nested=models.Nested( key="overwritten_in", filter=models.Filter(must=[models.FieldCondition( - key="by", match=models.MatchValue(value=b))]), + key="by", match=models.MatchValue(value=branch))]), ))) return models.Filter(must=must, should=should, must_not=must_not) ``` -Fork `A` and add a file that only `A` has: +`lookup` and `update` now take the ancestry instead of a bare branch: ```python -fork("A", "root", at_seq=2) -update("roadmap.md", "A", seq=0, content="Draft roadmap for the next two quarters. Not for publication.") +def lookup(file_name, ancestry): + points, _ = client.scroll( + collection_name="content", + scroll_filter=visibility_filter(ancestry, path=file_name), + limit=1, with_payload=True, + ) + return points[0] if points else None -lookup("roadmap.md", "A").payload["content"] # A's own file +def update(file_name, branch, seq, content, ancestry): + prev = lookup(file_name, ancestry) + if prev: + supersede(prev, by=branch, seq=seq) + client.upsert(collection_name="content", points=[models.PointStruct( + id=point_id(branch, seq, file_name), + vector=models.Document(text=content, model=MODEL), + payload={"path": file_name, "content": content, "branch": branch, + "seq": seq, "overwritten_in": []}, + )]) +``` + +Fork `A` and add a file only `A` has: + +```python +update("roadmap.md", "A", seq=0, content="Draft roadmap for the next two quarters. Not for publication.", ancestry=A_ancestry) + +lookup("roadmap.md", A_ancestry).payload["content"] # A's own file # Draft roadmap for the next two quarters. Not for publication. -lookup("roadmap.md", "root") # root never sees it +lookup("roadmap.md", root_ancestry) # root never sees it # None -lookup("pricing.md", "A").payload["content"] # inherited from root +lookup("pricing.md", A_ancestry).payload["content"] # inherited from root # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. ``` @@ -294,15 +313,15 @@ root ●──●──● seq 0–2 Nothing new is needed. `update` looks up the version `A` currently sees, which is `root`'s, and marks it, this time with `by: A`: ```python -update("pricing.md", "A", seq=1, content="The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO.") +update("pricing.md", "A", seq=1, content="The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO.", ancestry=A_ancestry) ``` -The mark `{by: A, seq: 1}` lands on `root`'s `pricing.md` point. Because the exclusion matches on `by`, it removes that version for `A` but not for `root`, whose filter only drops versions marked `by: root`. The same file now resolves differently on each branch: +The mark `{by: A, seq: 1}` lands on `root`'s `pricing.md` point. `A`'s exclusion drops it (it matches `by: A`), while `root`'s exclusion only drops marks with `by: root`, so `root` keeps its version. The same file now resolves differently on each branch: ```python -lookup("pricing.md", "A").payload["content"] +lookup("pricing.md", A_ancestry).payload["content"] # The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. -lookup("pricing.md", "root").payload["content"] +lookup("pricing.md", root_ancestry).payload["content"] # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. ``` @@ -315,13 +334,13 @@ root ●──●──●──────● root seq 3: update terms.md └── A ●──● A forked at seq 2, so it should not see seq 3 ``` +`root` commits the edit at `seq 3`: + ```python -update("terms.md", "root", seq=3, content="Updated terms of service, effective Q3, with the new data-retention clause.") +update("terms.md", "root", seq=3, content="Updated terms of service, effective Q3, with the new data-retention clause.", ancestry=root_ancestry) ``` -`A` forked at `root` `seq 2`, so it should not see anything `root` committed later, including this `seq 3` edit. The bound is the `seq` we've recorded all along: each ancestor counts only up to the `seq` where this branch forked from it.
The cutoff applies on both sides of the filter: the candidate clause includes an ancestor's versions only up to the fork `seq`, and the exclusion clause honors only the marks it stamped by then, so a later change can neither appear nor hide the version `A` kept. - -That cutoff is a range on `seq`, on both the points and the marks, so both fields need an index: +`A` forked at `root` `seq 2`, so it should not see anything `root` committed later, including this `seq 3` edit. This is where the fork `seq` in the ancestry earns its place: each ancestor counts only up to it.
The cutoff is a range on `seq`, applied to the points and to the marks: on the candidate side so `A` stops seeing `root`'s later versions, and on the exclusion side so a mark `root` added after the fork can't hide a version `A` kept. Both fields need an index: ```python client.create_payload_index( @@ -329,33 +348,25 @@ client.create_payload_index( field_schema=models.PayloadSchemaType.INTEGER, ) client.create_payload_index( - collection_name="content", field_name="overwritten_in.seq", + collection_name="content", field_name="overwritten_in[].seq", field_schema=models.PayloadSchemaType.INTEGER, ) ``` -The walk now carries each branch's cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form of the filter: +`visibility_filter` now reads the cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form: ```python -def ancestry(branch): - yield (branch, None) # the branch itself: all its own commits - node = branches[branch] - while node is not None: - parent, fork_seq = node - yield (parent, fork_seq) - node = branches[parent] - -def visibility_filter(branch, path=None): +def visibility_filter(ancestry, path=None): must = [] if path: must.append(models.FieldCondition( key="path", match=models.MatchValue(value=path))) should, must_not = [], [] - for b, cut in ancestry(branch): + for branch, cut in ancestry: candidate = [models.FieldCondition( - key="branch", match=models.MatchValue(value=b))] + key="branch", match=models.MatchValue(value=branch))] excluded = [models.FieldCondition( - key="by", match=models.MatchValue(value=b))] + key="by", match=models.MatchValue(value=branch))] if cut is not None: # an ancestor: only up to the fork point candidate.append(models.FieldCondition( key="seq", range=models.Range(lte=cut))) @@ -367,12 +378,12 @@ def visibility_filter(branch, path=None): return models.Filter(must=must, should=should, must_not=must_not) ``` -The candidate clause stops including `root`'s versions past `seq 2`, and the exclusion ignores marks stamped after `seq 2`. `A` keeps the `terms.md` it forked from; `root` sees the new one: +`A` keeps the `terms.md` it forked from; `root` sees the new one: ```python -lookup("terms.md", "A").payload["content"] +lookup("terms.md", A_ancestry).payload["content"] # Standard terms of service apply to all plans. -lookup("terms.md", "root").payload["content"] +lookup("terms.md", root_ancestry).payload["content"] # Updated terms of service, effective Q3, with the new data-retention clause. ``` @@ -386,19 +397,21 @@ root ●──●──●──────● root seq 0–3 └── B ● B forks at root seq 3; B seq 0: update pricing.md ``` +Record `B`'s ancestry and commit its version: + ```python -fork("B", "root", at_seq=3) -update("pricing.md", "B", seq=0, content="The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR.") +B_ancestry = [("B", None), ("root", 3)] +update("pricing.md", "B", seq=0, content="The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR.", ancestry=B_ancestry) ``` `root`'s `pricing.md` point now carries two marks, `{by: A, seq: 1}` and `{by: B, seq: 0}`. Each branch's exclusion matches only its own `by` on a single record, so the three branches resolve the same path three ways, with no leakage between the two forks: ```python -for branch in ["root", "A", "B"]: - print(branch, "→", lookup("pricing.md", branch).payload["content"]) -# root → The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. -# A → The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. -# B → The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. +for anc in [root_ancestry, A_ancestry, B_ancestry]: + print(lookup("pricing.md", anc).payload["content"]) +# The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +# The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. +# The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. ``` ## Searching a Branch @@ -406,11 +419,11 @@ for branch in ["root", "A", "B"]: `lookup` resolves one known path; a search ranks the whole corpus by relevance. It's the same visibility filter, passed to a semantic query instead of a path scroll: ```python -def search(query, branch, limit=5): +def search(query, ancestry, limit=5): return client.query_points( collection_name="content", query=models.Document(text=query, model=MODEL), - query_filter=visibility_filter(branch), + query_filter=visibility_filter(ancestry), limit=limit, with_payload=True, ).points ``` @@ -418,19 +431,19 @@ def search(query, branch, limit=5): The same query, scoped to each branch, returns that branch's live version of the page: ```python -for branch in ["root", "A", "B"]: - hit = search("how much does the pro plan cost", branch, limit=1)[0] - print(branch, "→", hit.payload["content"]) -# root → The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. -# A → The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. -# B → The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. +for anc in [root_ancestry, A_ancestry, B_ancestry]: + hit = search("how much does the pro plan cost", anc, limit=1)[0] + print(hit.payload["content"]) +# The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. +# The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. +# The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. ``` ## Scaling to Production -- **The filter grows with lineage depth, not corpus size.** Each query walks the lineage once, adding one candidate clause and one exclusion clause per ancestor. The filter's size tracks how deep a branch sits, not how many files or versions the collection holds. +- **The filter grows with lineage depth, not corpus size.** Each query walks the ancestry once, adding one candidate clause and one exclusion clause per ancestor. The filter's size tracks how deep a branch sits, not how many files or versions the collection holds. -- **Qdrant is a derived index.** Branch lineage and history live in version control. Build the collection by replaying that history, and if the index ever drifts, rebuild it. The derived point IDs make a full replay idempotent: the same commit writes the same point, so `upsert` overwrites cleanly. +- **Qdrant is a derived index.** Branch ancestry and history live in version control. Build the collection by replaying that history, and if the index ever drifts, rebuild it. The derived point IDs make a full replay idempotent: the same commit writes the same point, so `upsert` overwrites cleanly. - **Storage grows with edits.** Every overwrite keeps the old version as a point, so long-lived branches accumulate versions. Reclaiming superseded versions is a production concern. From c4b87e666b31e61746da953bfe24bf85867639d3 Mon Sep 17 00:00:00 2001 From: Dylan Couzon Date: Fri, 29 May 2026 10:42:53 -0400 Subject: [PATCH 08/10] Delete branch_aware_verifier.py --- branch_aware_verifier.py | 203 --------------------------------------- 1 file changed, 203 deletions(-) delete mode 100644 branch_aware_verifier.py diff --git a/branch_aware_verifier.py b/branch_aware_verifier.py deleted file mode 100644 index 0732f89b2..000000000 --- a/branch_aware_verifier.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -Verifier for the Branch-Aware Search tutorial. - -Replicates the tutorial's final functions and runs the seven-step fixture, -asserting every commented output. Runs in STRICT MODE (as Qdrant Cloud does), so -it actually exercises the payload-index requirements: the nested-array indexes -must be named overwritten_in[].by / overwritten_in[].seq or strict mode rejects -the filter. - -Only difference from the tutorial: the client points at a LOCAL Docker Qdrant -with local inference (cloud_inference defaults to False), so models.Document is -embedded client-side via fastembed. Operations, payload, filter, model, and index -names match the tutorial exactly. - -Run: .venv-verify/bin/python branch_aware_verifier.py -""" - -import uuid -from qdrant_client import QdrantClient, models - -MODEL = "sentence-transformers/all-MiniLM-L6-v2" -COLLECTION = "content" - -client = QdrantClient(url="http://localhost:6333") - -# --------------------------------------------------------------------------- -# Collection in strict mode + payload indexes (bracket notation for the -# nested-array fields, which strict mode requires). -# --------------------------------------------------------------------------- -if client.collection_exists(COLLECTION): - client.delete_collection(COLLECTION) - -client.create_collection( - collection_name=COLLECTION, - vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE), - strict_mode_config=models.StrictModeConfig( - enabled=True, unindexed_filtering_retrieve=False), -) - -for field_name, schema in [ - ("path", models.PayloadSchemaType.KEYWORD), - ("branch", models.PayloadSchemaType.KEYWORD), - ("seq", models.PayloadSchemaType.INTEGER), - ("overwritten_in[].by", models.PayloadSchemaType.KEYWORD), - ("overwritten_in[].seq", models.PayloadSchemaType.INTEGER), -]: - client.create_payload_index( - collection_name=COLLECTION, field_name=field_name, field_schema=schema) - -# --------------------------------------------------------------------------- -# Tutorial functions (final forms). Ancestry is passed in: a list of -# (branch, cutoff) from the branch itself (cutoff None) up to the root. -# --------------------------------------------------------------------------- -NS = uuid.UUID("00000000-0000-0000-0000-000000000042") - -def point_id(branch, seq, path): - return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}")) - -def supersede(point, by, seq): - marks = point.payload.get("overwritten_in", []) + [{"by": by, "seq": seq}] - client.set_payload( - collection_name=COLLECTION, - payload={"overwritten_in": marks}, - points=[point.id], - ) - -def visibility_filter(ancestry, path=None): - must = [] - if path: - must.append(models.FieldCondition( - key="path", match=models.MatchValue(value=path))) - should, must_not = [], [] - for branch, cut in ancestry: - candidate = [models.FieldCondition( - key="branch", match=models.MatchValue(value=branch))] - excluded = [models.FieldCondition( - key="by", match=models.MatchValue(value=branch))] - if cut is not None: # an ancestor: only up to the fork point - candidate.append(models.FieldCondition( - key="seq", range=models.Range(lte=cut))) - excluded.append(models.FieldCondition( - key="seq", range=models.Range(lte=cut))) - should.append(models.Filter(must=candidate)) - must_not.append(models.NestedCondition(nested=models.Nested( - key="overwritten_in", filter=models.Filter(must=excluded)))) - return models.Filter(must=must, should=should, must_not=must_not) - -def lookup(file_name, ancestry): - points, _ = client.scroll( - collection_name=COLLECTION, - scroll_filter=visibility_filter(ancestry, path=file_name), - limit=1, with_payload=True, - ) - return points[0] if points else None - -def update(file_name, branch, seq, content, ancestry): - prev = lookup(file_name, ancestry) - if prev: - supersede(prev, by=branch, seq=seq) - client.upsert(collection_name=COLLECTION, points=[models.PointStruct( - id=point_id(branch, seq, file_name), - vector=models.Document(text=content, model=MODEL), - payload={"path": file_name, "content": content, - "branch": branch, "seq": seq, "overwritten_in": []}, - )]) - -def delete(file_name, branch, seq, ancestry): - supersede(lookup(file_name, ancestry), by=branch, seq=seq) - -def search(query, ancestry, limit=5): - return client.query_points( - collection_name=COLLECTION, - query=models.Document(text=query, model=MODEL), - query_filter=visibility_filter(ancestry), - limit=limit, with_payload=True, - ).points - -# --------------------------------------------------------------------------- -# Assertions -# --------------------------------------------------------------------------- -PASS = [] - -def check(label, got, want): - assert got == want, f"\nFAIL: {label}\n got: {got!r}\n want: {want!r}" - PASS.append(label) - print(f"OK {label}") - -def content(file_name, ancestry): - p = lookup(file_name, ancestry) - return p.payload["content"] if p else None - -PRICING_V1 = "The Pro tier costs $29 per month with 100 GB of storage." -PRICING_V2 = "The Pro tier costs $39 per month with 200 GB of storage and unlimited seats." -PRICING_A = "The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO." -PRICING_B = "The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR." -NOTES = "Internal launch notes. Remove before the public release." -TERMS_V1 = "Standard terms of service apply to all plans." -TERMS_V2 = "Updated terms of service, effective Q3, with the new data-retention clause." -ROADMAP = "Draft roadmap for the next two quarters. Not for publication." - -ROOT = [("root", None)] -A = [("A", None), ("root", 2)] -B = [("B", None), ("root", 3)] - -# Step 1: add files on root (seq 0) -update("pricing.md", "root", seq=0, content=PRICING_V1, ancestry=ROOT) -update("notes.md", "root", seq=0, content=NOTES, ancestry=ROOT) -update("terms.md", "root", seq=0, content=TERMS_V1, ancestry=ROOT) -check("step 1 pricing.md@root == $29", content("pricing.md", ROOT), PRICING_V1) - -# Step 2: update pricing.md (seq 1) -update("pricing.md", "root", seq=1, content=PRICING_V2, ancestry=ROOT) -check("step 2 pricing.md@root == $39", content("pricing.md", ROOT), PRICING_V2) - -# Step 3: delete notes.md (seq 2) -delete("notes.md", "root", seq=2, ancestry=ROOT) -check("step 3 notes.md@root is None", content("notes.md", ROOT), None) - -# Step 4: fork A at root seq 2, add roadmap.md (A seq 0) -update("roadmap.md", "A", seq=0, content=ROADMAP, ancestry=A) -check("step 4 roadmap.md@A == draft", content("roadmap.md", A), ROADMAP) -check("step 4 roadmap.md@root is None", content("roadmap.md", ROOT), None) -check("step 4 pricing.md@A inherited", content("pricing.md", A), PRICING_V2) - -# Step 5: A updates inherited pricing.md (A seq 1) -update("pricing.md", "A", seq=1, content=PRICING_A, ancestry=A) -check("step 5 pricing.md@A == SSO", content("pricing.md", A), PRICING_A) -check("step 5 pricing.md@root unchanged", content("pricing.md", ROOT), PRICING_V2) - -# Step 6: root updates terms.md after the fork (seq 3) -update("terms.md", "root", seq=3, content=TERMS_V2, ancestry=ROOT) -check("step 6 terms.md@A pre-fork", content("terms.md", A), TERMS_V1) -check("step 6 terms.md@root new", content("terms.md", ROOT), TERMS_V2) - -# Step 7: fork B at root seq 3, overwrite pricing.md (B seq 0) -update("pricing.md", "B", seq=0, content=PRICING_B, ancestry=B) -check("step 7 pricing.md@root", content("pricing.md", ROOT), PRICING_V2) -check("step 7 pricing.md@A", content("pricing.md", A), PRICING_A) -check("step 7 pricing.md@B", content("pricing.md", B), PRICING_B) - -# Searching a branch (dense, same filter) -QUERY = "how much does the pro plan cost" -for anc, want, name in [(ROOT, PRICING_V2, "root"), (A, PRICING_A, "A"), (B, PRICING_B, "B")]: - top = search(QUERY, anc, limit=1)[0].payload["content"] - check(f"search cost@{name} top-1", top, want) - -# Search must not leak a hidden version even below rank 1 -for anc, hidden, name in [(A, PRICING_B, "A"), (B, PRICING_A, "B"), (ROOT, PRICING_A, "root")]: - seen = [p.payload["content"] for p in search(QUERY, anc, limit=5)] - check(f"search no leak of other branch @{name}", hidden in seen, False) - -# Step 7 leaves both marks on root's pricing.md point -root_pricing = lookup("pricing.md", ROOT) -marks = {(m["by"], m["seq"]) for m in lookup("pricing.md", ROOT) and - client.retrieve(COLLECTION, ids=[point_id("root", 1, "pricing.md")])[0].payload["overwritten_in"]} -check("marks on root pricing seq1 == {(A,1),(B,0)}", marks, {("A", 1), ("B", 0)}) - -# Physical points: 3 (root seq0) + 1 (root seq1) + 1 (root seq3) + 1 (A roadmap) -# + 1 (A pricing) + 1 (B pricing) = 8 (delete writes no point) -stored = client.count(COLLECTION, exact=True).count -check("physical points stored == 8", stored, 8) - -print(f"\nALL ASSERTIONS PASSED ({len(PASS)} checks). Physical points stored: {stored}") From 9c9c746a7fee43d44a8e1c93b6db533aca45e26d Mon Sep 17 00:00:00 2001 From: generall Date: Fri, 29 May 2026 17:42:23 +0200 Subject: [PATCH 09/10] review: restructure step 6 --- .../branch-aware-search.md | 155 +++++++++++++++--- 1 file changed, 135 insertions(+), 20 deletions(-) diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index 697420d21..5bf588625 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -353,40 +353,155 @@ client.create_payload_index( ) ``` -`visibility_filter` now reads the cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form: +`visibility_filter` now reads the cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. +This is the final form: ```python -def visibility_filter(ancestry, path=None): +def branch_filter( + branch: str, + ancestry: List[Tuple[str, int]] + ) -> models.Filter: + """ + Returns a filter, that matches only active + files in the current branch view. + + Ancestry is a list of (branch, fork_seq) + pairs, ordered from the branch itself + Example: + branch = "A" + ancestry = [("root", 2)] + + Simplified example of the retult: + + filter: { + should = [ + { "must": [ branch == "a" ] }, + { "must": [ branch == "root", seq <= 2 ] } + ] + must_not = [ + { "nested": overwritten_in { by == "a" } }, + { "nested": overwritten_in { by == "root", seq <= 2 } } + ] + } + + """ + ... + +``` + +
+ +Full filter code + +```python +def branch_filter( + branch: str, + ancestry: List[Tuple[str, int]] + ) -> models.Filter: + + # Selctor for the current branch + should = [ + model.FieldCondition( + key="branch", + match=model.MatchValue(value=branch) + ) + ] + + # Exclude files deleted in current branch + must_not = [ + models.NestedCondition( + nested=models.Nested( + key="overwritten_in", + filter=models.Filter( + must=[ + models.FieldCondition( + key="by", + match=models.MatchValue(value=branch) + ) + ] + ) + ) + ) + ] + + for branch, cut in ancestry: + branch_selector = models.Filter( + must=[ + models.FieldCondition( + key="branch", + match=models.MatchValue(value=branch) + ), + models.FieldCondition( + key="seq", + range=models.Range(lte=cut) + ) + ] + ) + should.append(branch_selector) + + overwrite_exclusion = models.NestedCondition( + nested=models.Nested( + key="overwritten_in", + filter=models.Filter( + must=[ + models.FieldCondition( + key="by", + match=models.MatchValue(value=branch) + ), + models.FieldCondition( + key="seq", + range=models.Range(lte=cut) + ) + ] + ) + ) + ) + must_not.append(overwrite_exclusion) + + + return models.Filter(should=should, must_not=must_not) + +``` + +and the full filter with optional file path would be constructed like this: + +```python + +def visibility_filter(branch, ancestry, path=None): must = [] if path: - must.append(models.FieldCondition( - key="path", match=models.MatchValue(value=path))) - should, must_not = [], [] - for branch, cut in ancestry: - candidate = [models.FieldCondition( - key="branch", match=models.MatchValue(value=branch))] - excluded = [models.FieldCondition( - key="by", match=models.MatchValue(value=branch))] - if cut is not None: # an ancestor: only up to the fork point - candidate.append(models.FieldCondition( - key="seq", range=models.Range(lte=cut))) - excluded.append(models.FieldCondition( - key="seq", range=models.Range(lte=cut))) - should.append(models.Filter(must=candidate)) - must_not.append(models.NestedCondition(nested=models.Nested( - key="overwritten_in", filter=models.Filter(must=excluded)))) - return models.Filter(must=must, should=should, must_not=must_not) + file_selector = models.FieldCondition( + key="path", + match=models.MatchValue(value=path) + ) + + must.append(file_selector) + + branch_selector = branch_filter(branch, ancestry) + must.append(branch_selector) + + return models.Filter(must=must) ``` +
+ `A` keeps the `terms.md` it forked from; `root` sees the new one: ```python lookup("terms.md", A_ancestry).payload["content"] # Standard terms of service apply to all plans. lookup("terms.md", root_ancestry).payload["content"] -# Updated terms of service, effective Q3, with the new data-retention clause. +# Updated terms of service, effective Q3, with the new data-retention clause ``` +Storage state: + +```javascript +{ path: 'terms.md', branch: root, seq: 0, overwritten_in: [{by: root, seq: 3}] } +{ path: 'terms.md', branch: root, seq: 3, overwritten_in: [] } +``` + + ## Step 7: A Second Branch, Same File Fork `B` off `root` and have it overwrite `pricing.md`, the same file `A` already changed. From 5c41638f7663d5266afd5f9622c7656c0beb8a46 Mon Sep 17 00:00:00 2001 From: generall Date: Fri, 29 May 2026 17:58:45 +0200 Subject: [PATCH 10/10] review: apply step 6 structure across all tutorial steps Reconcile every step to the parents-only ancestry model (branch passed separately), add docstrings with simplified filter results, hide full filter code under
, and show storage state at the end of each step. Also fix step 6 typos (models., Selector, result, "A") and the shadowed loop variable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../branch-aware-search.md | 355 ++++++++++++++---- 1 file changed, 273 insertions(+), 82 deletions(-) diff --git a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md index 5bf588625..0ba12e67c 100644 --- a/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md +++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md @@ -24,6 +24,7 @@ This tutorial uses Qd ```python from qdrant_client import QdrantClient, models +from typing import List, Tuple # 384-dim dense vectors, free on Cloud Inference MODEL = "sentence-transformers/all-MiniLM-L6-v2" @@ -118,6 +119,14 @@ lookup("pricing.md", "root").payload["content"] # The Pro tier costs $29 per month with 100 GB of storage. ``` +Storage state: + +```javascript +{ path: 'pricing.md', branch: root, seq: 0 } +{ path: 'notes.md', branch: root, seq: 0 } +{ path: 'terms.md', branch: root, seq: 0 } +``` + ## Step 2: Update a File A second commit changes a file that already exists. @@ -163,18 +172,73 @@ client.create_payload_index( ```python def visibility_filter(branch, path=None): - must = [models.FieldCondition( - key="branch", match=models.MatchValue(value=branch))] - if path: - must.append(models.FieldCondition( - key="path", match=models.MatchValue(value=path))) - replaced = models.NestedCondition(nested=models.Nested( - key="overwritten_in", - filter=models.Filter(must=[models.FieldCondition( - key="by", match=models.MatchValue(value=branch))]), - )) - return models.Filter(must=must, must_not=[replaced]) + """ + Returns a filter matching a branch's view of one + file (or of every file, when path is None). + Simplified example of the result: + + filter: { + must = [ + branch == "root", + path == "pricing.md" + ] + must_not = [ + { "nested": overwritten_in { by == "root" } } + ] + } + """ + ... +``` + +
+ +Full filter code + +```python +def visibility_filter(branch, path=None): + + # Match the requested branch (and file, if given) + must = [ + models.FieldCondition( + key="branch", + match=models.MatchValue(value=branch) + ) + ] + + if path: + must.append( + models.FieldCondition( + key="path", + match=models.MatchValue(value=path) + ) + ) + + # Drop versions this branch already replaced + must_not = [ + models.NestedCondition( + nested=models.Nested( + key="overwritten_in", + filter=models.Filter( + must=[ + models.FieldCondition( + key="by", + match=models.MatchValue(value=branch) + ) + ] + ) + ) + ) + ] + + return models.Filter(must=must, must_not=must_not) +``` + +
+ +`lookup` passes that filter to a scroll: + +```python def lookup(file_name, branch): points, _ = client.scroll( collection_name="content", @@ -195,13 +259,9 @@ lookup("pricing.md", "root").payload["content"] Both versions are still stored. The old one now carries the mark; the new one is clean: -```python -for p in client.retrieve("content", ids=[ - point_id("root", 0, "pricing.md"), - point_id("root", 1, "pricing.md")]): - print(p.payload["branch"], p.payload["seq"], p.payload["overwritten_in"]) -# root 0 [{'by': 'root', 'seq': 1}] -# root 1 [] +```javascript +{ path: 'pricing.md', branch: root, seq: 0, overwritten_in: [{by: root, seq: 1}] } +{ path: 'pricing.md', branch: root, seq: 1, overwritten_in: [] } ``` ## Step 3: Delete a File @@ -227,6 +287,12 @@ lookup("notes.md", "root") The point is still stored, but `notes.md` no longer resolves on `root`. That recorded `seq` is what will let a branch that forked before the delete keep the file. +Storage state: + +```javascript +{ path: 'notes.md', branch: root, seq: 0, overwritten_in: [{by: root, seq: 2}] } +``` + ## Step 4: Branch Off the Root A fork records where it started: the parent and the parent's latest `seq`. It writes no points; its view begins as whatever the parent has at that `seq`. @@ -236,48 +302,150 @@ root ●──●──● seq 0–2 └── A ● A forks at root seq 2; A seq 0: add roadmap.md ``` -A read on a branch now considers that branch and every ancestor up to its fork point. We pass that chain in as the branch's **ancestry**: `(branch, fork seq)` from the branch itself down to the root. Build it once when the branch is created and store it alongside the branch: +A read on a branch now considers that branch and every ancestor up to its fork point. The branch itself is passed separately; its **ancestry** lists only the parents as `(branch, fork seq)` pairs, ordered from the nearest parent down to the root. Build it once when the branch is created and store it alongside the branch: ```python -# None means no cutoff (the branch's own commits); an ancestor is capped -# at its fork seq. The fork seq starts mattering in Step 6. -root_ancestry = [("root", None)] -A_ancestry = [("A", None), ("root", 2)] +# Parents only, each capped at the fork seq. The current branch is +# passed separately and counts all of its own commits. +# The fork seq is stored now but only starts mattering in Step 6. +root_ancestry = [] # root has no parents +A_ancestry = [("root", 2)] # A forked at root seq 2 ``` -`visibility_filter` walks that ancestry, building one candidate clause and one exclusion per branch. The candidates (a `should`, so any one can match) gather each branch's versions; the exclusions (a `must_not`) drop anything that branch replaced: +Now that a read spans more than one branch, factor the branch logic into its own `branch_filter`. It builds one candidate clause and one exclusion per branch: the current branch first, then every ancestor. The candidates (a `should`, so any one can match) gather each branch's versions; the exclusions (a `must_not`) drop anything that branch replaced: ```python -def visibility_filter(ancestry, path=None): +def branch_filter( + branch: str, + ancestry: List[Tuple[str, int]] + ) -> models.Filter: + """ + Returns a filter that matches only the files + visible in the current branch's view. + + Ancestry is a list of (branch, fork_seq) pairs, + ordered from the nearest parent down to the root. + Example: + branch = "A" + ancestry = [("root", 2)] + + Simplified example of the result: + + filter: { + should = [ + { "must": [ branch == "A" ] }, + { "must": [ branch == "root" ] } + ] + must_not = [ + { "nested": overwritten_in { by == "A" } }, + { "nested": overwritten_in { by == "root" } } + ] + } + """ + ... +``` + +
+ +Full filter code + +```python +def branch_filter( + branch: str, + ancestry: List[Tuple[str, int]] + ) -> models.Filter: + + # Selector for the current branch + should = [ + models.FieldCondition( + key="branch", + match=models.MatchValue(value=branch) + ) + ] + + # Exclude files this branch overwrote or deleted + must_not = [ + models.NestedCondition( + nested=models.Nested( + key="overwritten_in", + filter=models.Filter( + must=[ + models.FieldCondition( + key="by", + match=models.MatchValue(value=branch) + ) + ] + ) + ) + ) + ] + + # One selector and one exclusion per ancestor + for parent, _cut in ancestry: + should.append( + models.Filter( + must=[ + models.FieldCondition( + key="branch", + match=models.MatchValue(value=parent) + ) + ] + ) + ) + + must_not.append( + models.NestedCondition( + nested=models.Nested( + key="overwritten_in", + filter=models.Filter( + must=[ + models.FieldCondition( + key="by", + match=models.MatchValue(value=parent) + ) + ] + ) + ) + ) + ) + + return models.Filter(should=should, must_not=must_not) +``` + +
+ +`visibility_filter` wraps it, adding the optional file path: + +```python +def visibility_filter(branch, ancestry, path=None): must = [] + if path: - must.append(models.FieldCondition( - key="path", match=models.MatchValue(value=path))) - should, must_not = [], [] - for branch, _ in ancestry: - should.append(models.Filter(must=[models.FieldCondition( - key="branch", match=models.MatchValue(value=branch))])) - must_not.append(models.NestedCondition(nested=models.Nested( - key="overwritten_in", - filter=models.Filter(must=[models.FieldCondition( - key="by", match=models.MatchValue(value=branch))]), - ))) - return models.Filter(must=must, should=should, must_not=must_not) + must.append( + models.FieldCondition( + key="path", + match=models.MatchValue(value=path) + ) + ) + + must.append(branch_filter(branch, ancestry)) + + return models.Filter(must=must) ``` -`lookup` and `update` now take the ancestry instead of a bare branch: +`lookup` and `update` now take the branch and its ancestry: ```python -def lookup(file_name, ancestry): +def lookup(file_name, branch, ancestry): points, _ = client.scroll( collection_name="content", - scroll_filter=visibility_filter(ancestry, path=file_name), + scroll_filter=visibility_filter(branch, ancestry, path=file_name), limit=1, with_payload=True, ) return points[0] if points else None def update(file_name, branch, seq, content, ancestry): - prev = lookup(file_name, ancestry) + prev = lookup(file_name, branch, ancestry) if prev: supersede(prev, by=branch, seq=seq) client.upsert(collection_name="content", points=[models.PointStruct( @@ -293,14 +461,20 @@ Fork `A` and add a file only `A` has: ```python update("roadmap.md", "A", seq=0, content="Draft roadmap for the next two quarters. Not for publication.", ancestry=A_ancestry) -lookup("roadmap.md", A_ancestry).payload["content"] # A's own file +lookup("roadmap.md", "A", A_ancestry).payload["content"] # A's own file # Draft roadmap for the next two quarters. Not for publication. -lookup("roadmap.md", root_ancestry) # root never sees it +lookup("roadmap.md", "root", root_ancestry) # root never sees it # None -lookup("pricing.md", A_ancestry).payload["content"] # inherited from root +lookup("pricing.md", "A", A_ancestry).payload["content"] # inherited from root # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. ``` +Storage state: + +```javascript +{ path: 'roadmap.md', branch: A, seq: 0, overwritten_in: [] } +``` + ## Step 5: Change an Inherited File on the Branch `A` edits `pricing.md`, a file it inherited from `root`. @@ -319,12 +493,20 @@ update("pricing.md", "A", seq=1, content="The Pro tier costs $39 per month with The mark `{by: A, seq: 1}` lands on `root`'s `pricing.md` point. `A`'s exclusion drops it (it matches `by: A`), while `root`'s exclusion only drops marks with `by: root`, so `root` keeps its version. The same file now resolves differently on each branch: ```python -lookup("pricing.md", A_ancestry).payload["content"] +lookup("pricing.md", "A", A_ancestry).payload["content"] # The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. -lookup("pricing.md", root_ancestry).payload["content"] +lookup("pricing.md", "root", root_ancestry).payload["content"] # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. ``` +Storage state: + +```javascript +{ path: 'pricing.md', branch: root, seq: 0, overwritten_in: [{by: root, seq: 1}] } +{ path: 'pricing.md', branch: root, seq: 1, overwritten_in: [{by: A, seq: 1}] } +{ path: 'pricing.md', branch: A, seq: 1, overwritten_in: [] } +``` + ## Step 6: Commit on the Root After the Fork `root` keeps moving after `A` forked off. It updates `terms.md`, a file `A` inherited but never touched. @@ -353,7 +535,7 @@ client.create_payload_index( ) ``` -`visibility_filter` now reads the cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. +`branch_filter` now reads the cutoff: the branch itself has none (all its own commits count), and each ancestor is bound to its fork `seq`. This is the final form: ```python @@ -362,28 +544,28 @@ def branch_filter( ancestry: List[Tuple[str, int]] ) -> models.Filter: """ - Returns a filter, that matches only active - files in the current branch view. + Returns a filter that matches only the files + visible in the current branch's view. - Ancestry is a list of (branch, fork_seq) - pairs, ordered from the branch itself + Ancestry is a list of (branch, fork_seq) pairs, + ordered from the nearest parent down to the root. + Each ancestor now counts only up to its fork seq. Example: branch = "A" ancestry = [("root", 2)] - Simplified example of the retult: + Simplified example of the result: filter: { should = [ - { "must": [ branch == "a" ] }, + { "must": [ branch == "A" ] }, { "must": [ branch == "root", seq <= 2 ] } ] must_not = [ - { "nested": overwritten_in { by == "a" } }, + { "nested": overwritten_in { by == "A" } }, { "nested": overwritten_in { by == "root", seq <= 2 } } ] } - """ ... @@ -399,15 +581,15 @@ def branch_filter( ancestry: List[Tuple[str, int]] ) -> models.Filter: - # Selctor for the current branch + # Selector for the current branch should = [ - model.FieldCondition( + models.FieldCondition( key="branch", - match=model.MatchValue(value=branch) + match=models.MatchValue(value=branch) ) ] - # Exclude files deleted in current branch + # Exclude files this branch overwrote or deleted must_not = [ models.NestedCondition( nested=models.Nested( @@ -424,12 +606,14 @@ def branch_filter( ) ] - for branch, cut in ancestry: + # One selector and one exclusion per ancestor, + # each capped at the fork seq + for parent, cut in ancestry: branch_selector = models.Filter( must=[ models.FieldCondition( key="branch", - match=models.MatchValue(value=branch) + match=models.MatchValue(value=parent) ), models.FieldCondition( key="seq", @@ -446,7 +630,7 @@ def branch_filter( must=[ models.FieldCondition( key="by", - match=models.MatchValue(value=branch) + match=models.MatchValue(value=parent) ), models.FieldCondition( key="seq", @@ -458,27 +642,25 @@ def branch_filter( ) must_not.append(overwrite_exclusion) - return models.Filter(should=should, must_not=must_not) ``` -and the full filter with optional file path would be constructed like this: +The `visibility_filter` wrapper is unchanged from Step 4; it still adds the optional file path on top of `branch_filter`: ```python - def visibility_filter(branch, ancestry, path=None): must = [] + if path: - file_selector = models.FieldCondition( - key="path", - match=models.MatchValue(value=path) + must.append( + models.FieldCondition( + key="path", + match=models.MatchValue(value=path) + ) ) - must.append(file_selector) - - branch_selector = branch_filter(branch, ancestry) - must.append(branch_selector) + must.append(branch_filter(branch, ancestry)) return models.Filter(must=must) ``` @@ -488,10 +670,10 @@ def visibility_filter(branch, ancestry, path=None): `A` keeps the `terms.md` it forked from; `root` sees the new one: ```python -lookup("terms.md", A_ancestry).payload["content"] +lookup("terms.md", "A", A_ancestry).payload["content"] # Standard terms of service apply to all plans. -lookup("terms.md", root_ancestry).payload["content"] -# Updated terms of service, effective Q3, with the new data-retention clause +lookup("terms.md", "root", root_ancestry).payload["content"] +# Updated terms of service, effective Q3, with the new data-retention clause. ``` Storage state: @@ -515,30 +697,39 @@ root ●──●──●──────● root seq 0–3 Record `B`'s ancestry and commit its version: ```python -B_ancestry = [("B", None), ("root", 3)] +B_ancestry = [("root", 3)] # B forked at root seq 3 update("pricing.md", "B", seq=0, content="The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR.", ancestry=B_ancestry) ``` `root`'s `pricing.md` point now carries two marks, `{by: A, seq: 1}` and `{by: B, seq: 0}`. Each branch's exclusion matches only its own `by` on a single record, so the three branches resolve the same path three ways, with no leakage between the two forks: ```python -for anc in [root_ancestry, A_ancestry, B_ancestry]: - print(lookup("pricing.md", anc).payload["content"]) +for branch, anc in [("root", root_ancestry), ("A", A_ancestry), ("B", B_ancestry)]: + print(lookup("pricing.md", branch, anc).payload["content"]) # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. # The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO. # The Pro tier costs $39 per month with 200 GB of storage. EU customers: data stays in-region under GDPR. ``` +Storage state: + +```javascript +{ path: 'pricing.md', branch: root, seq: 0, overwritten_in: [{by: root, seq: 1}] } +{ path: 'pricing.md', branch: root, seq: 1, overwritten_in: [{by: A, seq: 1}, {by: B, seq: 0}] } +{ path: 'pricing.md', branch: A, seq: 1, overwritten_in: [] } +{ path: 'pricing.md', branch: B, seq: 0, overwritten_in: [] } +``` + ## Searching a Branch `lookup` resolves one known path; a search ranks the whole corpus by relevance. It's the same visibility filter, passed to a semantic query instead of a path scroll: ```python -def search(query, ancestry, limit=5): +def search(query, branch, ancestry, limit=5): return client.query_points( collection_name="content", query=models.Document(text=query, model=MODEL), - query_filter=visibility_filter(ancestry), + query_filter=visibility_filter(branch, ancestry), limit=limit, with_payload=True, ).points ``` @@ -546,8 +737,8 @@ def search(query, ancestry, limit=5): The same query, scoped to each branch, returns that branch's live version of the page: ```python -for anc in [root_ancestry, A_ancestry, B_ancestry]: - hit = search("how much does the pro plan cost", anc, limit=1)[0] +for branch, anc in [("root", root_ancestry), ("A", A_ancestry), ("B", B_ancestry)]: + hit = search("how much does the pro plan cost", branch, anc, limit=1)[0] print(hit.payload["content"]) # The Pro tier costs $39 per month with 200 GB of storage and unlimited seats. # The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO.