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
- pg_textsearch - The PostgreSQL extension
- BM25 Algorithm - How ranking works
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)
| File | Size | Uploaded | |
|---|---|---|---|
| django_hawkeye-0.4.0.tar.gz | 27.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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