2828[ ![ Slack URL] ( https://img.shields.io/badge/Join%20Slack-purple?logo=slack&link=https%3A%2F%2Fparadedb.com%2Fslack )] ( https://paradedb.com/slack )
2929[ ![ X URL] ( https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2Fparadedb&label=Follow%20%40paradedb )] ( https://x.com/paradedb )
3030
31- [ ParadeDB] ( https://paradedb.com ) integration for SQLAlchemy: typed helpers for BM25 indexes, search predicates, scoring, snippets, facets, and migration ergonomics.
31+ The official [ SQLAlchemy] ( https://www.sqlalchemy.org/ ) integration for [ ParadeDB] ( https://paradedb.com ) including first class support for for managing BM25 indexes with Alembic and running queries using the full ParadeDB API.
32+
33+ ## Usage
34+
35+ Follow the [ getting started guide] ( https://docs.paradedb.com/documentation/getting-started/environment#sqlalchemy ) to begin.
3236
3337## Requirements & Compatibility
3438
3943| ParadeDB | 0.22.0+ |
4044| PostgreSQL | 15+ (with ParadeDB extension) |
4145
42- ## Installation
43-
44- ``` bash
45- uv add sqlalchemy-paradedb
46- ```
47-
48- For local development:
49-
50- ``` bash
51- uv sync --extra test --extra dev
52- ```
53-
54- ## Quick Start
55-
56- ### Prerequisites
57-
58- Install ` pg_search ` in your Postgres database and connect SQLAlchemy to that database.
59-
60- ### Create a BM25 Index
61-
62- ``` python
63- from sqlalchemy import Index
64- from paradedb.sqlalchemy import indexing
65-
66- products_bm25_idx = Index(
67- " products_bm25_idx" ,
68- indexing.BM25Field(Product.id),
69- indexing.BM25Field(
70- Product.description,
71- tokenizer = indexing.tokenize.unicode(lowercase = True ),
72- ),
73- indexing.BM25Field(
74- Product.category,
75- tokenizer = indexing.tokenize.literal(),
76- ),
77- postgresql_using = " bm25" ,
78- postgresql_with = {" key_field" : " id" },
79- )
80- ```
81-
82- For JSON columns named ` metadata ` , use ` metadata_ ` as the ORM attribute name.
83-
84- ### Query with ParadeDB Predicates
85-
86- ``` python
87- from sqlalchemy import select
88- from sqlalchemy.orm import Session
89- from paradedb.sqlalchemy import pdb, search
90-
91- stmt = (
92- select(Product.id, Product.description)
93- .where(search.match_any(Product.description, " running" , " shoes" ))
94- .order_by(pdb.score(Product.id).desc())
95- .limit(10 )
96- )
97-
98- with Session(engine) as session:
99- rows = session.execute(stmt).all()
100- ```
101-
102- ### Rows + Facets in a Single Query
103-
104- ``` python
105- from sqlalchemy import select
106- from sqlalchemy.orm import Session
107- from paradedb.sqlalchemy import facets, search
108-
109- base = (
110- select(Product.id, Product.description)
111- .where(search.match_all(Product.description, " running" ))
112- .order_by(Product.id)
113- .limit(10 )
114- )
115-
116- stmt = facets.with_rows(
117- base,
118- agg = facets.multi(
119- facets.value_count(field = " id" ),
120- facets.terms(field = " category" , size = 10 ),
121- ),
122- key_field = Product.id,
123- )
124-
125- with Session(engine) as session:
126- rows = session.execute(stmt).all()
127- facet_payload = facets.extract(rows)
128- ```
129-
130- ## Search Patterns
131-
132- ### Fuzzy Matching
133-
134- ``` python
135- from paradedb.sqlalchemy import search
136-
137- search.term(Product.description, " shose" , distance = 1 )
138- search.match_any(Product.description, " wirless" , distance = 1 , prefix = True )
139- search.term(Product.description, " rnnuing" , distance = 1 , transpose_cost_one = True )
140- ```
141-
142- Use fuzzy options on ` term ` , ` match_any ` , or ` match_all ` ; there is no separate ` search.fuzzy(...) ` helper.
143-
144- ### Phrase Prefix and More-Like-This
145-
146- ``` python
147- from paradedb.sqlalchemy import search
148-
149- search.phrase_prefix(Product.description, [" running" , " sh" ])
150- search.more_like_this(Product.id, document_id = 1 , fields = [" description" ])
151- ```
152-
153- ### Proximity Composition
154-
155- ``` python
156- from sqlalchemy import select
157- from paradedb.sqlalchemy import search
158-
159- prox = search.prox_array(" running" ).within(1 , search.prox_regex(" sho.*" ), ordered = True )
160- stmt = select(Product.id).where(search.proximity(Product.description, prox))
161- ```
162-
163- ## Indexing and Tokenizers
164-
165- Tokenizer config can be expressed as a structured mapping:
166-
167- ``` python
168- from sqlalchemy import Index
169- from paradedb.sqlalchemy import indexing
170-
171- products_bm25_idx = Index(
172- " products_bm25_idx" ,
173- indexing.BM25Field(Product.id),
174- indexing.BM25Field(
175- Product.description,
176- tokenizer = indexing.tokenize.from_config(
177- {
178- " tokenizer" : " simple" ,
179- " filters" : [" lowercase" , " stemmer" ],
180- " stemmer" : " english" ,
181- " alias" : " description_simple" ,
182- }
183- ),
184- ),
185- indexing.BM25Field(
186- Product.description,
187- tokenizer = indexing.tokenize.from_config(
188- {
189- " tokenizer" : " ngram" ,
190- " args" : [3 , 8 ],
191- " named_args" : {" prefix_only" : True },
192- " alias" : " description_ngram" ,
193- }
194- ),
195- ),
196- postgresql_using = " bm25" ,
197- postgresql_with = {" key_field" : " id" },
198- )
199- ```
200-
201- Validate that a field is indexed with the expected tokenizer:
202-
203- ``` python
204- from paradedb.sqlalchemy import indexing
205-
206- indexing.assert_indexed(engine, Product.category, tokenizer = " literal" )
207- ```
208-
209- Inspect BM25 metadata for a mapped table:
210-
211- ``` python
212- from paradedb.sqlalchemy import indexing
213-
214- meta = indexing.describe(engine, Product.__table__)
215- ```
216-
217- ## Alembic Operations
218-
219- Import once in migration environment startup so Alembic registers ParadeDB operations:
220-
221- ``` python
222- import paradedb.sqlalchemy.alembic # noqa: F401
223- ```
224-
225- Use custom operations in migrations:
226-
227- ``` python
228- op.create_bm25_index(
229- " products_bm25_idx" ,
230- " products" ,
231- [" id" , " description" ],
232- key_field = " id" ,
233- table_schema = " public" ,
234- )
235- op.reindex_bm25(" products_bm25_idx" , concurrently = True , schema = " public" )
236- op.drop_bm25_index(" products_bm25_idx" , if_exists = True , schema = " public" )
237- ```
238-
239- ` op.reindex_bm25(..., concurrently=True) ` must run outside a transaction (autocommit block).
240-
241- ## Diagnostics Helpers
242-
243- ` paradedb.sqlalchemy.diagnostics ` exposes wrapper functions for ParadeDB diagnostics:
244-
245- ``` python
246- from paradedb.sqlalchemy import diagnostics
247-
248- indexes = diagnostics.paradedb_indexes(engine)
249- segments = diagnostics.paradedb_index_segments(engine, " products_bm25_idx" )
250- check = diagnostics.paradedb_verify_index(engine, " products_bm25_idx" , sample_rate = 0.1 )
251- all_checks = diagnostics.paradedb_verify_all_indexes(engine, schema_pattern = " public" )
252- ```
253-
254- ## Common Errors
255-
256- ### ` with_rows requires ORDER BY `
257-
258- ``` python
259- from sqlalchemy import select
260- from paradedb.sqlalchemy import facets
261-
262- # Missing order_by(...)
263- base = select(Product.id).limit(10 )
264- facets.with_rows(base, agg = facets.value_count(field = " id" ), key_field = Product.id)
265- ```
266-
267- ### ` with_rows requires LIMIT `
268-
269- ``` python
270- from sqlalchemy import select
271- from paradedb.sqlalchemy import facets
272-
273- # Missing limit(...)
274- base = select(Product.id).order_by(Product.id)
275- facets.with_rows(base, agg = facets.value_count(field = " id" ), key_field = Product.id)
276- ```
277-
278- ### ` with_rows requires a ParadeDB predicate `
279-
280- ``` python
281- from sqlalchemy import select
282- from paradedb.sqlalchemy import facets
283-
284- # ensure_predicate=False disables automatic search.all(...) injection
285- facets.with_rows(
286- select(Product.id).order_by(Product.id).limit(10 ),
287- agg = facets.value_count(field = " id" ),
288- key_field = Product.id,
289- ensure_predicate = False ,
290- )
291- ```
292-
293- ### ` tokenizer config requires 'tokenizer' `
294-
295- ``` python
296- from paradedb.sqlalchemy import indexing
297-
298- indexing.tokenize.from_config({" filters" : [" lowercase" ]})
299- ```
300-
30146## Examples
30247
30348- [ Quick Start] ( examples/quickstart/quickstart.py )
@@ -307,11 +52,6 @@ indexing.tokenize.from_config({"filters": ["lowercase"]})
30752- [ Hybrid Search (RRF)] ( examples/hybrid_rrf/hybrid_rrf.py )
30853- [ RAG] ( examples/rag/rag.py )
30954
310- ## Documentation
311-
312- - ** ParadeDB Official Docs** : < https://docs.paradedb.com >
313- - ** ParadeDB Website** : < https://paradedb.com >
314-
31555## Contributing
31656
31757See [ CONTRIBUTING.md] ( CONTRIBUTING.md ) for development setup, running tests, linting, and the PR workflow.
0 commit comments