Skip to main content

django-hawkeye 🎯

Django BM25 full-text search using PostgreSQL pg_textsearch - a lightweight Elasticsearch alternative.

Features

  • Simple API - Just add a mixin and search with Article.search("query")
  • BM25 ranking - Industry-standard relevance scoring (same as Elasticsearch)
  • No external services - Uses PostgreSQL 17+ native search
  • RAG-ready - Use as the retrieval layer for Retrieval Augmented Generation

Requirements

  • PostgreSQL 17+
  • pg_textsearch extension
  • Django 4.2+
  • Python 3.10+

Installation

pip install django-hawkeye

PostgreSQL Extension Setup

This library requires the pg_textsearch extension installed on your PostgreSQL server:

# Install build dependencies
apt-get install build-essential git postgresql-server-dev-17

# Clone and build
git clone https://github.com/timescale/pg_textsearch.git
cd pg_textsearch
make && make install

The extension is automatically enabled via Django migrations when you run python manage.py migrate.

See the pg_textsearch repository for detailed installation instructions.

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'django_hawkeye',
]

Quick Start

1. Define your model

from django.db import models
from django_hawkeye import BM25Index, BM25Searchable

class Article(BM25Searchable, models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()

    class Meta:
        indexes = [
            BM25Index(fields=['content'], name='article_bm25_idx'),
        ]

2. Run migrations

python manage.py makemigrations
python manage.py migrate

3. Search

# Basic search
Article.search("django tutorial")

# With filters
Article.search("web framework").filter(published=True)[:10]

# With score threshold (lower = better match)
Article.search("django").filter(bm25_score__lt=-1.0)

API

BM25Searchable Mixin

Add to any model to enable .search() method:

class Article(BM25Searchable, models.Model):
    ...

BM25Index

BM25Index(
    fields=['content'],
    name='article_bm25_idx',
    text_config='english',  # PostgreSQL text search config
    k1=1.2,                 # Term frequency saturation (0.1-10.0)
    b=0.75,                 # Length normalization (0.0-1.0)
)

Search Methods

# Basic search - returns BM25SearchQuerySet
Article.search("query")

# Chainable with Django QuerySet methods
Article.search("query").filter(author="John")
Article.search("query").exclude(draft=True)
Article.search("query").select_related('author')
Article.search("query")[:10]  # Limit results

# Filter by score threshold
Article.search("query").filter(bm25_score__lt=-1.0)

Advanced Usage

Override search() method

class Article(BM25Searchable, models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()

    class Meta:
        indexes = [
            BM25Index(fields=['content'], name='article_bm25_idx'),
        ]

    @classmethod
    def search(cls, query, include_title=False):
        """Custom search with optional title filtering."""
        results = super().search(query)
        if include_title:
            results = results.filter(title__icontains=query)
        return results

Direct Expression API

Use BM25Score for full control:

from django_hawkeye import BM25Score

# Manual annotation
Article.objects.annotate(
    score=BM25Score('content', 'search query', index_name='article_bm25_idx')
).order_by('score')

# Multi-field weighted search
from django.db.models import F

Article.objects.annotate(
    title_score=BM25Score('title', query, index_name='title_idx'),
    content_score=BM25Score('content', query, index_name='content_idx'),
).annotate(
    combined=F('title_score') * 2 + F('content_score')
).order_by('combined')

Without Mixin

from django_hawkeye import BM25Index, BM25Score

class Article(models.Model):
    content = models.TextField()

    class Meta:
        indexes = [
            BM25Index(fields=['content'], name='article_bm25_idx'),
        ]

    @classmethod
    def search(cls, query):
        return cls.objects.annotate(
            score=BM25Score('content', query, index_name='article_bm25_idx')
        ).filter(score__lt=0).order_by('score')

Score Semantics

pg_textsearch returns NEGATIVE scores. Lower values = better match.

# Correct - ascending order (best matches first)
Article.search("query")  # Already ordered correctly

# Manual ordering
.order_by('bm25_score')  # ✓ Correct
.order_by('-bm25_score') # ✗ Wrong - worst matches first

Why Hawkeye?

Feature Elasticsearch django-hawkeye
Infrastructure Separate cluster Your PostgreSQL
Sync Manual index sync Automatic (native)
Cost $$$ Free
Setup Complex Add mixin + migrate
BM25 ranking ✓ ✓

License

MIT

Links

Release files for django-hawkeye 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-hawkeye 0.4.0
File Size Uploaded
django_hawkeye-0.4.0.tar.gz 27.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-hawkeye 0.4.0
File Interpreter ABI Platform
django_hawkeye-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 39.4 kB

Release files / django_hawkeye-0.4.0.tar.gz

Download URL django_hawkeye-0.4.0.tar.gz
Size 27.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4d060580624176909be00b627978375924890ff912fccd2292cf9dcc0fac2923
BLAKE2b-256 checksum
How to use checksums
6a947e564ff9c4274ed3b9c26ebe76a15a6b6d644d6c04e0de74a6a7a9e4d4b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 7, 2026.

Transparency log

Release files / django_hawkeye-0.4.0-py3-none-any.whl

Download URL django_hawkeye-0.4.0-py3-none-any.whl
Size 12.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a6b0d8e9615d815102dca3094f0b86360b834f7b75ff283bf3154e32947297e4
BLAKE2b-256 checksum
How to use checksums
f870d9fe7463529a184898c0a516f70ebedda631db384e0995900bc120c1f36d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page