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..198f7acb4 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 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 5d3457377..50d4f7183 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 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
new file mode 100644
index 000000000..0ba12e67c
--- /dev/null
+++ b/qdrant-landing/content/documentation/tutorials-search-engineering/branch-aware-search.md
@@ -0,0 +1,762 @@
+---
+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 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/
+---
+
+# Branch-Aware Search Over Versioned Documents
+
+| Time: 25 min | Level: Intermediate |
+| --- | ----------- |
+
+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
+
+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.
+
+
+
+```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"
+
+# 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=models.VectorParams(
+ size=384, distance=models.Distance.COSINE),
+)
+```
+
+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")
+
+def point_id(branch: str, seq: int, path: str) -> str:
+ return str(uuid.uuid5(NS, f"{branch}|{seq}|{path}"))
+```
+
+
+
+## Step 1: Add a File
+
+Start with a single `root` branch and one commit that adds a file.
+
+```text
+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,
+)
+```
+
+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},
+ )])
+```
+
+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
+
+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.
+
+```text
+root ●───────● seq 1: update pricing.md
+ seq 0 seq 1
+```
+
+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.get("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 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",
+ field_schema=models.PayloadSchemaType.KEYWORD,
+)
+```
+
+`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):
+ """
+ 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",
+ scroll_filter=visibility_filter(branch, path=file_name),
+ limit=1, with_payload=True,
+ )
+ return points[0] if points else None
+```
+
+`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.")
+
+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:
+
+```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
+
+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
+
+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.
+
+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`.
+
+```text
+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. 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
+# 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
+```
+
+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 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)
+ )
+ )
+
+ must.append(branch_filter(branch, ancestry))
+
+ return models.Filter(must=must)
+```
+
+`lookup` and `update` now take the branch and its ancestry:
+
+```python
+def lookup(file_name, branch, ancestry):
+ points, _ = client.scroll(
+ collection_name="content",
+ 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, branch, 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", A_ancestry).payload["content"] # A's own file
+# Draft roadmap for the next two quarters. Not for publication.
+lookup("roadmap.md", "root", root_ancestry) # root never sees it
+# None
+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`.
+
+```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.", ancestry=A_ancestry)
+```
+
+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", A_ancestry).payload["content"]
+# The Pro tier costs $39 per month with 200 GB of storage, unlimited seats, and SSO.
+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.
+
+```text
+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.", ancestry=root_ancestry)
+```
+
+`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(
+ 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,
+)
+```
+
+`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
+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.
+ Each ancestor now counts only up to its fork seq.
+ Example:
+ branch = "A"
+ ancestry = [("root", 2)]
+
+ Simplified example of the result:
+
+ 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:
+
+ # 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,
+ # each capped at the fork seq
+ for parent, cut in ancestry:
+ branch_selector = models.Filter(
+ must=[
+ models.FieldCondition(
+ key="branch",
+ match=models.MatchValue(value=parent)
+ ),
+ 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=parent)
+ ),
+ models.FieldCondition(
+ key="seq",
+ range=models.Range(lte=cut)
+ )
+ ]
+ )
+ )
+ )
+ must_not.append(overwrite_exclusion)
+
+ return models.Filter(should=should, must_not=must_not)
+
+```
+
+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:
+ must.append(
+ models.FieldCondition(
+ key="path",
+ match=models.MatchValue(value=path)
+ )
+ )
+
+ must.append(branch_filter(branch, ancestry))
+
+ return models.Filter(must=must)
+```
+
+
+
+`A` keeps the `terms.md` it forked from; `root` sees the new one:
+
+```python
+lookup("terms.md", "A", A_ancestry).payload["content"]
+# Standard terms of service apply to all plans.
+lookup("terms.md", "root", root_ancestry).payload["content"]
+# 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.
+
+```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
+```
+
+Record `B`'s ancestry and commit its version:
+
+```python
+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 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, branch, ancestry, limit=5):
+ return client.query_points(
+ collection_name="content",
+ query=models.Document(text=query, model=MODEL),
+ query_filter=visibility_filter(branch, ancestry),
+ limit=limit, with_payload=True,
+ ).points
+```
+
+The same query, scoped to each branch, returns that branch's live version of the page:
+
+```python
+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.
+# 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 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 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.
+
+- **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, 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.