---
title: "How do I do hybrid search in Python?"
description: "Hybrid search in Python: rank by embedding similarity, then filter on metadata on the same table. BM25 fusion stays a dedicated search engine."
url: "https://www.pixeltable.com/learn/how-to/hybrid-search-in-python"
updated: "2026-10-14"
doc: "https://docs.pixeltable.com/howto/cookbooks/search/search-semantic-text"
---

# How do I do hybrid search in Python?

Hybrid search combines more than one signal. The usual pair is dense similarity plus a structured filter so a semantic hit still respects the row metadata. BM25-plus-dense fusion is a different product: a dedicated search engine that mixes lexical and vector scores. You need an index; you need that engine only when fusion is the contract.

Updated: 2026-10-14
Part of [How do I ... in Python?](https://www.pixeltable.com/learn/how-to).

## Before you start

About 15 min.

- Python 3.11+
- pip install -U 'pixeltable[serve]'
- pxt init in the project root
- sentence-transformers

## On this page

- [Before you start](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#before-you-start)
- [Keep the vector and the filter on one row](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#step-1)
- [Filter metadata, then rank by similarity](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#step-2)
- [Keep BM25 fusion on the engine that owns it](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#step-3)
- [Comparison](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#comparison)
- [Where Pixeltable fits](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#where-pixeltable-fits)
- [Questions](https://www.pixeltable.com/learn/how-to/hybrid-search-in-python#questions)

## 1. Keep the vector and the filter on one row {#step-1}

Hybrid search fails when the embedding lives in one store and the tenant id lives in another. Put content, category, and the index on the same table.

```python
import pixeltable as pxt
from pixeltable.functions.huggingface import sentence_transformer

TableModel = pxt.model_base()
text_embed = sentence_transformer.using(model_id='all-MiniLM-L6-v2')

class Notes(TableModel, name='notes'):
    title: pxt.String
    content: pxt.String
    category: pxt.String
    __indexes__ = [
        pxt.EmbeddingIndex(content, string_embed=text_embed),
    ]
```

## 2. Filter metadata, then rank by similarity {#step-2}

where() restricts the rows. similarity(string=) ranks the rest. That is hybrid search as dense plus structured predicates, documented as combining semantic search with metadata filters.

```python
@pxt.query
def hybrid_search(query: str, category: str, limit: int = 5):
    sim = Notes.content.similarity(string=query)
    return (
        Notes.where(Notes.category == category)
        .order_by(sim, asc=False)
        .limit(limit)
        .select(Notes.title, Notes.category, Notes.content, sim)
    )
```

## 3. Keep BM25 fusion on the engine that owns it {#step-3}

Weaviate query.hybrid fuses BM25 with dense similarity (alpha balances the two). That is a different contract. Do not invent a BM25 API on the table. Hand vectors to Weaviate when that fusion is the product.

```python
# Pixeltable: dense similarity + metadata on the source row (this file).
# Weaviate: query.hybrid(query=..., alpha=0.5) on a collection.
# See /compare/pixeltable-vs-weaviate for that split.
```

## Two meanings of hybrid search {#comparison}

|  | Dense plus metadata | BM25 plus dense fusion |
| --- | --- | --- |
| Signals | Embedding score and structured filters | Lexical score fused with embedding score |
| Where it runs | The table that holds the passage | A search engine of objects and vectors |
| Typical API | where() then similarity(string=) | query.hybrid with alpha |
| Choose it when | Tenant, date, or category must constrain hits | Keyword recall plus meaning is the product |

## Where Pixeltable fits {#where-pixeltable-fits}

Pixeltable does hybrid search as similarity plus where() on the source row. It does not ship BM25-plus-dense fusion. Keep Weaviate when that engine is the serving contract; the compare page states the split. A separate vector database is still optional for ordinary RAG.

```python
import pixeltable as pxt
from pixeltable.functions.huggingface import sentence_transformer

TableModel = pxt.model_base()
text_embed = sentence_transformer.using(model_id='all-MiniLM-L6-v2')

class Notes(TableModel, name='notes'):
    content: pxt.String
    category: pxt.String
    __indexes__ = [
        pxt.EmbeddingIndex(content, string_embed=text_embed),
    ]

@pxt.query
def hybrid_search(query: str, category: str, limit: int = 5):
    sim = Notes.content.similarity(string=query)
    return (
        Notes.where(Notes.category == category)
        .order_by(sim, asc=False)
        .limit(limit)
        .select(Notes.content, Notes.category, sim)
    )
```

## Concepts

- [What is semantic search?](https://www.pixeltable.com/learn/what-is-semantic-search)
- [How does an embedding index differ from a vector database?](https://www.pixeltable.com/learn/embedding-index-vs-vector-database)
- [Vector database alternatives for multimodal apps](https://www.pixeltable.com/learn/vector-database-alternatives-for-multimodal-apps)

## Use case

- [Multimodal AI Apps: Process Any Data Type in One System](https://www.pixeltable.com/use-cases/multimodal-ai-application-development)

## Questions {#questions}

### Does Pixeltable implement BM25 hybrid fusion? {#faq-1}

No. Hybrid search here means dense similarity plus metadata filters on the same table. BM25-plus-dense fusion is Weaviate query.hybrid. Pixeltable vs Weaviate is the page for that engine.

### Can I filter before ranking? {#faq-2}

Yes. where() restricts rows; order_by on similarity ranks the rest. That is the documented pattern for combining semantic search with metadata filters.

### Is hybrid search the same as two embedding models? {#faq-3}

Not necessarily. Two models on two columns are two indexes you query separately. Hybrid as used here is one dense index plus structured predicates. CLIP plus a caption index is a different, valid pattern.

## Related

- [What is semantic search?](https://www.pixeltable.com/learn/what-is-semantic-search)
- [Embedding index vs a vector database](https://www.pixeltable.com/learn/embedding-index-vs-vector-database)
- [Vector database alternatives for multimodal apps](https://www.pixeltable.com/learn/vector-database-alternatives-for-multimodal-apps)
- [Multimodal application development](https://www.pixeltable.com/use-cases/multimodal-ai-application-development)
- [How do I do semantic search in Python?](https://www.pixeltable.com/learn/how-to/semantic-search-in-python)
- [Pixeltable vs Weaviate](https://www.pixeltable.com/compare/pixeltable-vs-weaviate)
- [Pixeltable vs Pinecone](https://www.pixeltable.com/compare/pixeltable-vs-pinecone)
- [Build semantic search for text](https://docs.pixeltable.com/howto/cookbooks/search/search-semantic-text)
