Skip to content

Commit 0cddfe7

Browse files
authored
Merge pull request #6 from svalench/rel-0.3
Rel 0.3
2 parents 9b21ac9 + 4292d69 commit 0cddfe7

51 files changed

Lines changed: 2586 additions & 141 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/scratchpad.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Scratchpad
2+
3+
Pylint CI: `_parse_float_param` один return + `math.isnan`; `_stringify_numeric_param`; переносы строк в views; фикстуры через `fixture(name=...)` (W0621); pgvector/settings/tasks длина строк; `tasks.delete_instance_task` сигнатура.
4+
5+
DONE — `pylint $(git ls-files '*.py')` 10/10; pytest по затронутым модулям 16 passed. Полный pytest: 2 fail в `test_langgraph_search` (Searcher + LANGGRAPH), вне этого PR.
6+
7+
Prerelease **0.3.0a1**: `setup.cfg` + `CHANGELOG.md`; `python -m build``dist/*.whl` и `dist/*.tar.gz` (папка в `.gitignore`).

.idea/.gitignore

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/django_graph_search.iml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/profiles_settings.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,52 @@ All notable changes to **django-graph-search** are documented here.
55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
66
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
## [0.3.0a1] — 2026-05-18
11+
12+
**Pre-release** of the upcoming **0.3.0** line. Install for smoke tests:
13+
14+
`pip install --pre django-graph-search==0.3.0a1`
15+
16+
### Added
17+
- **REST search:** each hit includes ``score`` (0.0–1.0) and ``text``; optional query param
18+
``min_score`` filters weak matches; response may include ``min_score_applied``.
19+
- **Model weights:** ``weight_fields`` is always parsed (including with ``fields: "__all__"``);
20+
weight ``0.0`` excludes a field from indexed text.
21+
- **Async indexing:** ``ASYNC_INDEXING`` settings (Celery / daemon thread / django-q) plus
22+
``django_graph_search.tasks`` helpers so ``AUTO_INDEX`` signals can avoid blocking requests.
23+
- **pgvector backend:** ``django_graph_search.backends.PgvectorBackend`` (extra ``[pgvector]``).
24+
- **Cloud embeddings:** ``OpenAIEmbeddingBackend`` and ``CohereEmbeddingBackend`` (extras
25+
``[openai]``, ``[cohere]``); Cohere distinguishes query vs document embeddings via ``is_query``.
26+
27+
### Changed
28+
- **Vector scores:** ChromaDB / FAISS / Qdrant backends normalize stored distances into
29+
similarity-style scores in the 0–1 range for consistent API output.
30+
31+
### Security
32+
- **REST API access control:** new optional ``GRAPH_SEARCH["API"]`` settings
33+
(``PERMISSION_CLASSES``, ``THROTTLE_CLASSES``, ``THROTTLE_RATES``,
34+
``REQUIRE_AUTHENTICATION``) with pluggable checks in
35+
``django_graph_search.permissions``. Search, streaming, and conversational
36+
views run these checks before handling requests. Defaults are empty / false so
37+
behaviour stays open unless you configure restrictions.
38+
- **Safe integer parsing for ``limit``:** invalid or negative ``limit`` values on
39+
search, streaming, conversational, and similar endpoints return HTTP 400
40+
instead of raising ``ValueError`` (500). Values above 1000 are clamped with a
41+
warning in logs.
42+
43+
### Fixed
44+
- **ChromaDB:** cosine collections use ``hnsw:space=cosine`` metadata; query distances mapped to similarity.
45+
- **File delta cache TTL:** ``FileDeltaCache`` now stores ``expires_at``,
46+
enforces expiry on read (lazy delete), and supports ``purge_expired(dry_run=)``
47+
plus the ``purge_search_cache`` management command for file backends.
48+
- **Conversational memory registry:** per-process memory backends are cached in a
49+
module-level registry with a lock (replacing a class attribute). A
50+
``RuntimeWarning`` is emitted when ``CONVERSATIONAL.MEMORY_BACKEND="inmemory"``,
51+
conversational search is enabled, and ``DEBUG`` is false, to highlight
52+
multi-worker limitations.
53+
854
## [0.2.0] — 2026-05-08
955

1056
A large feature release built around an **optional LangGraph orchestration layer**.
@@ -136,5 +182,6 @@ and signal handlers behave exactly as before.
136182
- REST endpoints `/api/search/` and `/api/search/similar/<model>/<pk>/`.
137183
- `build_search_index` management command.
138184

185+
[0.3.0a1]: https://github.com/svalench/django_graph_search/releases/tag/v0.3.0a1
139186
[0.2.0]: https://github.com/svalench/django_graph_search/releases/tag/v0.2.0
140187
[0.1.2]: https://github.com/svalench/django_graph_search/releases/tag/v0.1.2

README.md

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ pip install django-graph-search[faiss]
3434
# Qdrant backend (production, scalable)
3535
pip install django-graph-search[qdrant]
3636

37-
# All backends
37+
# pgvector (PostgreSQL extension)
38+
pip install django-graph-search[pgvector]
39+
40+
# OpenAI / Cohere cloud embeddings (no local PyTorch model)
41+
pip install django-graph-search[openai]
42+
pip install django-graph-search[cohere]
43+
44+
# All backends + LangGraph
3845
pip install django-graph-search[all]
3946
```
4047

@@ -62,8 +69,9 @@ GRAPH_SEARCH = {
6269
"follow_relations": True,
6370
"relation_depth": 2,
6471
},
65-
# Or index all concrete fields:
66-
# {"model": "shop.Review", "fields": "__all__"}
72+
# Or index all concrete fields (weight_fields still apply by field name):
73+
# {"model": "shop.Review", "fields": "__all__",
74+
# "weight_fields": {"title": 2.0, "body": 1.0, "internal_note": 0.0}},
6775
],
6876
"VECTOR_STORE": {
6977
"BACKEND": "django_graph_search.backends.ChromaDBBackend",
@@ -91,9 +99,14 @@ GRAPH_SEARCH = {
9199
"OPTIONS": {"path": "graph_search_cache"},
92100
"TTL": 86400,
93101
},
102+
# Optional REST hardening — permissions / throttling (see "Securing the REST API"):
103+
# "API": { ... },
94104
}
95105
```
96106

107+
To restrict access to the main search, streaming, and conversational HTTP endpoints,
108+
add an `"API"` block as described [below](#securing-the-rest-api-optional).
109+
97110
### 3. Add URLs
98111

99112
```python
@@ -116,7 +129,7 @@ python manage.py build_search_index
116129

117130
```bash
118131
# REST API
119-
GET /api/search/?q=wireless+headphones&models=shop.Product&limit=5
132+
GET /api/search/?q=wireless+headphones&models=shop.Product&limit=5&min_score=0.75
120133

121134
# Find similar items
122135
GET /api/search/similar/shop.Product/42/?limit=5
@@ -162,10 +175,96 @@ similar = get_similar(product_instance, limit=5)
162175

163176
| Endpoint | Method | Description |
164177
|---|---|---|
165-
| `/api/search/?q=...&models=...&limit=...` | `GET` | Semantic full-text search |
178+
| `/api/search/?q=...&models=...&limit=...&min_score=...` | `GET` | Semantic search; optional `min_score` (0.0–1.0) drops weaker hits |
166179
| `/api/search/similar/{app}.{Model}/{id}/` | `GET` | Find similar objects |
167180
| `/api/search/conversation/` | `POST` | Session-aware conversational search (optional, see below) |
168181
| `/api/search/conversation/?conversation_id=...` | `DELETE` | Clear a conversation history |
182+
| `/api/search/stream/` | `GET`, `POST` | Streaming search events (optional) |
183+
184+
Each result object includes **`model`**, **`pk`**, **`score`** (0.0–1.0 similarity), and **`text`**
185+
(the indexed document string). When `min_score` is used, the response also contains
186+
**`min_score_applied`**.
187+
188+
### Query parameters (`limit`)
189+
190+
The `limit` parameter controls how many results are returned (where supported):
191+
192+
| Where | Parameter |
193+
|---|---|
194+
| `/api/search/` | Query string `limit` |
195+
| `/api/search/` | Query string `min_score` (optional, float 0.0–1.0) |
196+
| `/api/search/similar/.../` | Query string `limit` |
197+
| `/api/search/stream/` | Query string or JSON/form body `limit` |
198+
| `/api/search/conversation/` | JSON/form field `limit` |
199+
200+
Rules:
201+
202+
- Must be a **positive integer** in the range **1–1000**. Values greater than **1000**
203+
are **clamped to 1000** and a warning is logged.
204+
- Invalid values (non-numeric strings, negative numbers, booleans, etc.) produce
205+
**HTTP 400** with JSON `{"error": "'limit' must be a positive integer."}`.
206+
- If `min_score` is set on **`/api/search/`**, only results with **`score >= min_score`**
207+
are returned. The JSON body includes **`min_score_applied`** with the threshold used.
208+
Invalid values return **HTTP 400**.
209+
210+
### Optional embedding backends (OpenAI / Cohere)
211+
212+
Instead of downloading a **sentence-transformers** model, you can point ``EMBEDDINGS`` at
213+
``django_graph_search.embeddings.OpenAIEmbeddingBackend`` or
214+
``django_graph_search.embeddings.CohereEmbeddingBackend`` (extras ``[openai]`` / ``[cohere]``).
215+
Cohere uses asymmetric ``input_type``: indexing uses document mode and search uses query mode
216+
(``embed_batch(..., is_query=False)`` vs ``embed(..., is_query=True)``).
217+
218+
### Async indexing from signals (optional)
219+
220+
When ``AUTO_INDEX`` is on, saves can block on large graphs. Enable ``ASYNC_INDEXING`` to offload work:
221+
222+
```python
223+
"ASYNC_INDEXING": {
224+
"ENABLED": True,
225+
"BACKEND": "celery", # or "thread" | "django_q"
226+
"CELERY_QUEUE": "search_indexing",
227+
"CELERY_TASK_PATH": "django_graph_search.tasks.index_instance_task",
228+
"CELERY_DELETE_TASK_PATH": "django_graph_search.tasks.delete_instance_task",
229+
},
230+
```
231+
232+
With ``thread``, indexing runs in a daemon thread (no retries). With ``celery``, install Celery
233+
and register tasks; if Celery is missing, the task module falls back to synchronous execution with a warning.
234+
235+
### Securing the REST API (optional)
236+
237+
**Scope:** Settings under `GRAPH_SEARCH["API"]` apply only to **`GET /api/search/`**,
238+
**`/api/search/stream/`**, **`POST`** and **`DELETE /api/search/conversation/`**.
239+
They do **not** apply to **`/api/search/similar/.../`** — protect that route separately
240+
(e.g. Django middleware, URL-level decorators, nginx, or wrapping in your own authenticated view).
241+
242+
By default the search endpoints remain **public** (backward compatible). Configure
243+
``GRAPH_SEARCH["API"]`` to add authentication, permissions, and throttling:
244+
245+
```python
246+
GRAPH_SEARCH = {
247+
# ... existing keys ...
248+
"API": {
249+
"REQUIRE_AUTHENTICATION": True,
250+
"PERMISSION_CLASSES": [
251+
# "rest_framework.permissions.IsAuthenticated", # if DRF is installed
252+
# or a dotted path to a callable(request) -> bool
253+
],
254+
"THROTTLE_CLASSES": [
255+
"django_graph_search.permissions.SimpleScopedRateThrottle",
256+
],
257+
"THROTTLE_RATES": {
258+
"search": "60/minute",
259+
"search_authenticated": "300/minute",
260+
},
261+
},
262+
}
263+
```
264+
265+
``SimpleScopedRateThrottle`` applies **in-process** limits (per Gunicorn worker).
266+
For accurate global limits across workers, use DRF cache-backed throttles or a
267+
reverse-proxy rate limit.
169268

170269
## Management Commands
171270

@@ -174,6 +273,8 @@ python manage.py build_search_index # Index all configured mode
174273
python manage.py build_search_index --model shop.Product # Index one model
175274
python manage.py clear_search_index # Remove all vectors
176275
python manage.py search_index_status # Show index statistics
276+
python manage.py purge_search_cache # Remove expired file delta cache (CACHE.BACKEND=file)
277+
python manage.py purge_search_cache --dry-run # Count expired entries without deleting
177278
```
178279

179280
## Admin UI
@@ -187,6 +288,9 @@ After installation, navigate to `/admin/graph-search/` for a semantic search int
187288
| ChromaDB | Development, small-medium datasets | No |
188289
| FAISS | High-speed CPU search, offline | No |
189290
| Qdrant | Production, large datasets, filtering | Yes |
291+
| **pgvector** (`django_graph_search.backends.PgvectorBackend`) | Same PostgreSQL as Django, no separate vector server | PostgreSQL + `vector` extension |
292+
293+
Install: `pip install django-graph-search[pgvector]`. Table is created automatically on first use (see backend docstring for `VECTOR_STORE.OPTIONS`).
190294

191295
## Delta Indexing & Cache
192296

@@ -198,6 +302,14 @@ Enable `DELTA_INDEXING: True` to skip objects that haven’t changed since last
198302
| `redis` | `OPTIONS.alias` | Production |
199303
| `db` | `OPTIONS.alias` | Simple setup |
200304

305+
With **`CACHE.BACKEND: "file"`**, each delta entry stores an **`expires_at`**
306+
timestamp derived from **`CACHE.TTL`**. Expired entries are removed **lazily** when read;
307+
the directory can still grow if keys are never re-read — run
308+
`python manage.py purge_search_cache` periodically (or via cron), or use
309+
`--dry-run` to count stale files without deleting. Redis/db backends use Django’s
310+
cache TTL and do not require this command; `purge_search_cache` only affects the
311+
file backend.
312+
201313
## LangGraph-powered search pipeline (optional)
202314

203315
Starting with this version, `django-graph-search` ships with an **optional**
@@ -266,13 +378,23 @@ GRAPH_SEARCH = {
266378
# ... existing config ...
267379
"CONVERSATIONAL": {
268380
"ENABLED": True,
269-
"MEMORY_BACKEND": "inmemory", # or "cache" / dotted path.
381+
"MEMORY_BACKEND": "redis",
382+
"MEMORY_OPTIONS": {
383+
"alias": "default", # Django CACHES alias
384+
"key_prefix": "dgs_conv",
385+
"ttl": 3600,
386+
},
270387
"MAX_HISTORY_ITEMS": 10,
271388
"ALLOW_CLARIFICATIONS": True,
272389
},
273390
}
274391
```
275392

393+
For local development and tests, ``MEMORY_BACKEND: "inmemory"`` is fine. With
394+
``DEBUG=False`` (typical production), the library emits a ``RuntimeWarning`` if
395+
in-memory mode is left enabled — switch to ``redis`` (Django cache → Redis) so
396+
every Gunicorn worker shares the same conversation state.
397+
276398
Endpoint: `POST /api/search/conversation/`
277399

278400
```json

0 commit comments

Comments
 (0)