---
type: Notebook
title: 02 — Hybrid Search Patterns
description: >-
  Code-only tour of the retrieval knobs in the Fractal RAG stack: the
  target_vector choice (content, title or summary), the alpha keyword-vector
  balance, source_type and year filters, and near_object expansion from an
  anchor hit.
resource: https://tyson-swetnam.github.io/fractal-notebooks/fractal-rag/notebooks/02_hybrid_search/
tags: [notebook, rag, weaviate, knowledge, jupyter]
companion_page: https://tyson-swetnam.github.io/fractal-notebooks/fractal-rag/
notebook:
  source: https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb
  raw: https://raw.githubusercontent.com/tyson-swetnam/fractal-notebooks/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb
  nbviewer: https://nbviewer.org/github/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb
  colab: https://colab.research.google.com/github/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb
  kernel: Python 3
  environment: fractal_rag package (pip install -e fractal_rag) with a running Weaviate instance
  cells:
    markdown: 7
    code: 6
    executed: 0
    figures: 0
    errors: 0
  outputs: none (code-only page; the notebook needs a live service to run)
sources:
  - id: ipynb
    resource: https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb
    title: 02_hybrid_search.ipynb
    last_modified: "2026-05-24T00:00:00Z"
generated:
  by: process:scripts/render_notebooks.py
  at: "2026-05-24T00:00:00Z"
status: stable
---

<!-- Generated by scripts/render_notebooks.py from the notebook named in the frontmatter. Do not edit: edit the .ipynb and re-run the script. -->

# 02 — Hybrid Search Patterns

!!! info "Jupyter notebook"

    This page shows the code of `02_hybrid_search.ipynb` without outputs: the notebook needs a running service to execute and is not run at build time.

    [View on GitHub](https://github.com/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb) · [Open in nbviewer](https://nbviewer.org/github/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb) · [Open in Colab](https://colab.research.google.com/github/tyson-swetnam/fractal-notebooks/blob/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb) · [Raw .ipynb](https://raw.githubusercontent.com/tyson-swetnam/fractal-notebooks/main/docs/fractal-rag/notebooks/02_hybrid_search.ipynb)

    Kernel: Python 3 · Environment: fractal_rag package (pip install -e fractal_rag) with a running Weaviate instance · Companion page: [fractal-rag/](https://tyson-swetnam.github.io/fractal-notebooks/fractal-rag/)

This notebook digs into the three knobs that most change retrieval quality:

1. `target_vector` — `content_vec` vs `title_vec` vs `summary_vec`.
2. `alpha` — slider between pure-keyword (0.0) and pure-vector (1.0).
3. `filters` — restrict by `source_type`, `year`, or other properties.

We'll run the same query under different settings so you can build intuition
for when to reach for which.


```python
from fractal_rag.client import client_session
from fractal_rag.config import get_settings
from weaviate.classes.query import Filter, MetadataQuery

settings = get_settings(refresh=True)
QUERY = 'Barnsley fern iterated function system'

def hybrid(target_vector, alpha=0.5, filters=None, limit=5):
    with client_session() as client:
        art = client.collections.get(settings.artifact_collection)
        return art.query.hybrid(
            query=QUERY, limit=limit, target_vector=target_vector,
            alpha=alpha, filters=filters,
            return_metadata=MetadataQuery(distance=True),
        ).objects

def show(objs, label):
    print(f'--- {label} ---')
    for o in objs:
        p = o.properties
        print(f"  [{p.get('source_type'):>10}] {p.get('title')[:70]}")
```

## target_vector matters

The same query against different named vectors returns different rankings. Use `title_vec` for 'find me the notebook/page called X' and `content_vec` for 'find passages about X'.


```python
show(hybrid('content_vec'), 'content_vec, alpha=0.5')
show(hybrid('title_vec'),   'title_vec,   alpha=0.5')
show(hybrid('summary_vec'), 'summary_vec, alpha=0.5')
```

## alpha shifts the keyword/vector balance

`alpha=0.0` is pure BM25 (lexical), `alpha=1.0` is pure cosine. For technical vocabulary that the embedding model may not know, lower alpha. For paraphrases and conceptual matches, raise alpha.


```python
show(hybrid('content_vec', alpha=0.0), 'alpha=0.0 (keyword)')
show(hybrid('content_vec', alpha=0.5), 'alpha=0.5 (balanced)')
show(hybrid('content_vec', alpha=1.0), 'alpha=1.0 (vector only)')
```

## Filtering by source_type

Often you want notebooks **only**, or articles **only**. Pass a `Filter` from `weaviate.classes.query`.


```python
notebook_only = Filter.by_property('source_type').contains_any(['notebook'])
articles_only = Filter.by_property('source_type').contains_any(['json_corpus'])

show(hybrid('content_vec', filters=notebook_only), 'notebooks only')
show(hybrid('content_vec', filters=articles_only), 'json_corpus only')
```

## Filtering by year

Useful for historical asks. Combine with `Filter.all_of(...)` if you need multiple conditions.


```python
before_1990 = Filter.all_of([
    Filter.by_property('source_type').contains_any(['json_corpus']),
    Filter.by_property('year').less_or_equal(1990),
])
show(hybrid('content_vec', filters=before_1990, limit=8), 'pre-1990 articles')
```

## near_object: 'more like this'

Once you find a single strong hit, `near_object` returns its neighbors in the embedding space. This is the workhorse for synthesis: find one anchor, then expand.


```python
anchor = hybrid('content_vec', limit=1)[0]
with client_session() as client:
    art = client.collections.get(settings.artifact_collection)
    neighbors = art.query.near_object(
        near_object=str(anchor.uuid),
        limit=5,
        target_vector='content_vec',
        return_metadata=MetadataQuery(distance=True),
    ).objects

print(f"Anchor: {anchor.properties.get('title')[:70]}")
print('Neighbors:')
for n in neighbors:
    d = getattr(n.metadata, 'distance', None)
    print(f"  [{d:.3f}] {n.properties.get('title')[:70]}")
```

## Next steps

- [03 — Concept ontology](03_concept_ontology.md) — follow cross-refs to find every artifact that mentions a given concept.
