surreal-orm-lite 0.34.1__tar.gz → 0.35.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/CHANGELOG.md +144 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/PKG-INFO +341 -38
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/README.md +340 -37
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/pyproject.toml +1 -1
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/__init__.py +8 -1
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/connection_manager.py +105 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/model_base.py +272 -5
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/query_set.py +329 -72
- surreal_orm_lite-0.35.0/src/surreal_orm_lite/search.py +1390 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/subquery.py +4 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/transaction.py +18 -5
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/.gitignore +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/LICENSE +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/Makefile +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/_sdk.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/auth.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/cache.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/changefeed.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/concurrency.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/connection_config.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/decoding.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/define_parser.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/edge.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/embedded.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/encoding.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/functions.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/geo.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/introspection.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/live.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/model_auth.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/model_ddl.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/model_generator.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/q.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/routing.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/schema.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/schema_state.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/signals.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/surql_literal.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/testing/__init__.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/testing/database.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/testing/fixtures.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/time_travel.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/types.py +0 -0
- {surreal_orm_lite-0.34.1 → surreal_orm_lite-0.35.0}/src/surreal_orm_lite/utils.py +0 -0
|
@@ -5,6 +5,150 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.35.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
Full-text search: a queryset searches text with the full SurrealDB-ORM's `search()`, and scores and
|
|
11
|
+
highlights the matches with `SearchScore` and `SearchHighlight` (plus `SearchOffsets`), over the
|
|
12
|
+
analyzers and full-text indexes a model declares with `Analyzer` and `Index` and applies with
|
|
13
|
+
`define_indexes()`. SurrealDB tokenises, matches, scores (BM25) and highlights on the server; the
|
|
14
|
+
ORM binds every search text and highlight tag as a query variable. The same code runs on SurrealDB
|
|
15
|
+
2.x, 3.x and the embedded engine, each line getting its own index keyword. The first minor of Tier
|
|
16
|
+
3's search phase.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`QuerySet.search(**field_queries)`**, the full SurrealDB-ORM's method: keeps the records whose
|
|
21
|
+
fields match. Each keyword names a field — a Python field name (an aliased field searches its
|
|
22
|
+
column) or a dotted path — and the text to match, bound, never written into the statement. The
|
|
23
|
+
terms of one text are AND-ed, and so are the fields; each field gets the next match reference, in
|
|
24
|
+
call order and across calls. `search()` applies wherever `filter()` does, AND-ed after
|
|
25
|
+
`filter()`, `Q` and `nearby()`: `exec()`, `first()`, `get()` without an id, `count()`,
|
|
26
|
+
`exists()`, the aggregations, `values()` groups, `bulk_update()`, `bulk_delete()`, `patch()` and a
|
|
27
|
+
`Subquery`, whose references start again at 0 (the full ORM applies it to `exec()` only).
|
|
28
|
+
`get(<id>)` and `all()` ignore it, as they ignore `filter()`.
|
|
29
|
+
- **`SearchScore(ref)` and `SearchHighlight(open_tag, close_tag, ref)`**, the full ORM's search
|
|
30
|
+
annotations, **and `SearchOffsets(ref)`**, an addition (SurrealDB's `search::offsets()`), in
|
|
31
|
+
`annotate()`: the BM25 score (a `float`), the text with each matched term between the tags (bound
|
|
32
|
+
as query variables), and where the matched terms sit. `exec()`, `first()` and `get()` without an
|
|
33
|
+
id set each value as an attribute named after its alias, never written back (`model_dump()`,
|
|
34
|
+
`save()` and `update()` leave it out); `order_by("-relevance")` sorts on an alias. A reference no
|
|
35
|
+
`search()` gave, or an alias that would shadow a field, its column or an attribute of the model,
|
|
36
|
+
raises `ValueError` before any request.
|
|
37
|
+
- **`Analyzer` and `Index`**, with the argument names of the full ORM's `DefineAnalyzer` and
|
|
38
|
+
`CreateIndex`: an analyzer's name, tokenizers (`blank`, `camel`, `class`, `punct`), filters
|
|
39
|
+
(`ascii`, `lowercase`, `uppercase`, `ngram`, `edgengram`, `snowball`, `mapper`), `function=` and
|
|
40
|
+
`comment=`; a plain index, a `UNIQUE` one (`unique=True`) or a full-text one (`search_analyzer=`,
|
|
41
|
+
an `Analyzer` or the name of an analyzer the database defines), which covers one field and takes
|
|
42
|
+
`bm25=` (`True`, or a `(k1, b)` pair) and `highlights=True`. Every argument is checked at
|
|
43
|
+
construction, more strictly than the server where its answer would be silently wrong.
|
|
44
|
+
- **`SurrealConfigDict(indexes=[Index(...)])`** declares a model's indexes, checked when the class
|
|
45
|
+
is defined (distinct names, one full-text index per column, one `Analyzer` per analyzer name);
|
|
46
|
+
`get_indexes()` returns them in declaration order, and `Model.index_ddl()` renders their
|
|
47
|
+
`DEFINE ANALYZER` and `DEFINE INDEX` statements without a connection, for SurrealDB 3.x or, with
|
|
48
|
+
`line=2`, for 2.x.
|
|
49
|
+
- **`await Model.define_indexes()`** applies them on the model's connection: it reads what the
|
|
50
|
+
database holds (`INFO FOR DB`, `INFO FOR TABLE`) and, when a full-text index is declared, the
|
|
51
|
+
server line (`client.version()`), then defines what is missing and redefines what changed, one
|
|
52
|
+
statement per request, and returns the statements applied — `[]` on an unchanged database, so it
|
|
53
|
+
is safe at start-up. `overwrite=False` defines only what is missing (`IF NOT EXISTS`); `tx=` joins
|
|
54
|
+
a transaction. Nothing is removed: **`await Model.remove_index(name)`** removes an index by name
|
|
55
|
+
(`REMOVE INDEX IF EXISTS`; `missing_ok=False` raises `SurrealDbNotFoundError` for a missing one).
|
|
56
|
+
- **`SurrealDBConnectionManager.define_analyzer(analyzer)` and `remove_analyzer(analyzer)`**, for
|
|
57
|
+
the analyzers, which belong to the database rather than to a table: `define_analyzer()` defines
|
|
58
|
+
one on the current connection, redefining it only when it changed, and returns the statements
|
|
59
|
+
applied; `remove_analyzer()` removes one by name and refuses one an index uses.
|
|
60
|
+
- README section 36, "Full-text search (`search()`, `SearchScore`, `SearchHighlight`, `Analyzer`,
|
|
61
|
+
`Index`)" (declaring and applying, searching, scoring and highlighting, what SurrealDB decides,
|
|
62
|
+
changing an analyzer, transactions, live queries, what is not supported, a note for users of the
|
|
63
|
+
full SurrealDB-ORM), seven rows of the "ORM behaviour: SurrealDB 2.x vs 3.x" table and one of the
|
|
64
|
+
embedded engine's, and the example notebook `examples/v0.35.0_full_text_search.ipynb`.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
ENGLISH = Analyzer("english", ["class", "blank"], ["lowercase", "ascii", "snowball(english)"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Article(BaseSurrealModel):
|
|
71
|
+
model_config = SurrealConfigDict(
|
|
72
|
+
table_name="article",
|
|
73
|
+
indexes=[Index("article_title", "title", search_analyzer=ENGLISH, bm25=True, highlights=True)],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
id: str | None = None
|
|
77
|
+
title: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
await Article.define_table()
|
|
81
|
+
await Article.define_indexes() # FULLTEXT on SurrealDB 3.x, SEARCH on 2.x; [] when nothing changed
|
|
82
|
+
articles = await (
|
|
83
|
+
Article.objects()
|
|
84
|
+
.search(title="physics")
|
|
85
|
+
.annotate(relevance=SearchScore(0), snippet=SearchHighlight("<b>", "</b>", 0))
|
|
86
|
+
.order_by("-relevance")
|
|
87
|
+
.exec()
|
|
88
|
+
)
|
|
89
|
+
# each article carries .relevance (a float) and .snippet (its title, "<b>physics</b>" highlighted)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **A transaction error no longer quotes a bound search text**: a failed statement whose reason
|
|
95
|
+
reports a missing full-text index ("no suitable index supporting the expression") is cut after
|
|
96
|
+
the expression's operator (`… the expression: slug @0@`), since SurrealDB 3.x quotes the bound
|
|
97
|
+
value after it. The server's text stays reachable as the error's suppressed `__context__`, as
|
|
98
|
+
for a reason that quotes the query.
|
|
99
|
+
|
|
100
|
+
### Notes
|
|
101
|
+
|
|
102
|
+
- **The index keyword follows the server line**, read from `client.version()`: `FULLTEXT` on
|
|
103
|
+
SurrealDB 3.x, `SEARCH` on 2.x and the embedded engine, each of which refuses the other's. The ORM
|
|
104
|
+
writes one field per full-text index on both lines.
|
|
105
|
+
- **Only what changed is redefined**: an unchanged index is never redefined, so start-up never
|
|
106
|
+
rebuilds. A rebuild re-reads the table: seconds per thousands of records on SurrealDB 2.x and the
|
|
107
|
+
embedded engine.
|
|
108
|
+
- **A changed analyzer rebuilds its indexes**: SurrealDB keeps the tokens of the records an index
|
|
109
|
+
holds when their analyzer changes, so a search would miss them. When `define_indexes()` or
|
|
110
|
+
`define_analyzer()` redefines an analyzer, it rebuilds every index of the database that uses it
|
|
111
|
+
(`REBUILD INDEX`) and logs one warning naming them (logger `surreal_orm_lite.search`). In a
|
|
112
|
+
transaction, `define_indexes(tx=…)` refuses an analyzer to define or redefine, and
|
|
113
|
+
`define_analyzer(tx=…)` a redefinition with indexes to rebuild, before any DDL: call
|
|
114
|
+
`define_analyzer()` without `tx=` first.
|
|
115
|
+
- **`remove_analyzer()` is safe on SurrealDB 2.x**: 2.x and the embedded engine accept removing an
|
|
116
|
+
analyzer an index uses, then refuse every write to the table; the ORM refuses it first on every
|
|
117
|
+
line, with SurrealDB 3.x's own message.
|
|
118
|
+
- **A live query's searched subquery is checked**: before a live query whose `Subquery` searches,
|
|
119
|
+
the ORM checks that each searched field has a full-text index and raises `SurrealDbError` naming
|
|
120
|
+
the field otherwise, before the live query starts.
|
|
121
|
+
- **BM25 scores depend on the line**: a term found in more than half the documents scores `0.0` on
|
|
122
|
+
SurrealDB 3.x and below zero on 2.x and the embedded engine, where a text holding such a term
|
|
123
|
+
therefore scores lower than without it. On 2.x and the embedded engine, BM25 statistics count an
|
|
124
|
+
index's records again at each rebuild or redefinition and keep counting deleted records, so the
|
|
125
|
+
scores depend on the index's history (the matches do not): a documented 2.x limitation. SurrealDB
|
|
126
|
+
3.x keeps them exact; compare scores within one query, and upgrade to 3.x where scores must not
|
|
127
|
+
depend on the index's history.
|
|
128
|
+
- **Refused before any request** (`SurrealDbError`): `search()` with `as_of()`, in either order and
|
|
129
|
+
in a subquery under it (the index holds the records' current text); with `live()` and `watch()`,
|
|
130
|
+
since SurrealDB accepts a live query whose condition holds a full-text match but never notifies
|
|
131
|
+
it — search in a `Subquery` instead, which notifies the records created or updated to match; and
|
|
132
|
+
with `changes()`. A search on a field without a full-text index raises `SurrealDbError` naming the
|
|
133
|
+
field instead of the server's error, which on SurrealDB 3.x quotes the search text, in a
|
|
134
|
+
transaction too. **Indexes on materialized views** are refused (`ValueError` when the class is
|
|
135
|
+
defined): SurrealDB 3.x does not index the records a view gains after the index is defined.
|
|
136
|
+
- **Call `define_table()` before `define_indexes()`**: on a table that does not exist yet,
|
|
137
|
+
SurrealDB's `DEFINE INDEX` creates it `TYPE ANY SCHEMALESS PERMISSIONS NONE`, which
|
|
138
|
+
`define_table()` replaces but `define_table(overwrite=False)` leaves as it is.
|
|
139
|
+
- **SurrealDB 3.1** (tested with 3.1.5 for backward compatibility): two `search()` fields AND-ed
|
|
140
|
+
can also return records that match only one of them, scored `0.0` on the other; a field without a
|
|
141
|
+
full-text index answers no record instead of an error; and a live query whose subquery searches
|
|
142
|
+
such a field starts.
|
|
143
|
+
- **Deferred**: `search()` with `as_of()` on a versioned SurrealDB 3.x server; indexes on
|
|
144
|
+
materialized views once SurrealDB indexes the records a view gains; 3.x's boolean match operators
|
|
145
|
+
(`@AND@`/`@OR@`) and `highlight_all`; `indexes=[Index(...)]` in generated models;
|
|
146
|
+
`define_index()`/`remove_index()` on the connection manager for tables without a model;
|
|
147
|
+
`Index(concurrently=True)`. Vector indexes come in v0.36.0, `hybrid_search()` in v0.37.0, and
|
|
148
|
+
analyzers and indexes in migrations in v0.38.0.
|
|
149
|
+
- Server pins unchanged: tested against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine,
|
|
150
|
+
plus 3.1.5 for backward compatibility.
|
|
151
|
+
|
|
8
152
|
## [0.34.1] - 2026-10-22
|
|
9
153
|
|
|
10
154
|
Follow-ups to the review of v0.34.0's model table DDL (#296): `define_access()` and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
4
4
|
Summary: Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
@@ -269,6 +269,7 @@ embedded engine refuse one above `4294967295` (`2**32 - 1`), SurrealDB 3.x reads
|
|
|
269
269
|
| Materialized views (`view_query`, `define_view()`, read-only) | ✅ |
|
|
270
270
|
| Relation constraints (`relation_in`, `define_relation()`, `TYPE RELATION`) | ✅ |
|
|
271
271
|
| Model table DDL (`define_table()`, `schema_mode`, `permissions`) | ✅ |
|
|
272
|
+
| Full-text search (`search()`, BM25 score and highlights) | ✅ |
|
|
272
273
|
|
|
273
274
|
### Supported Filter Lookups
|
|
274
275
|
|
|
@@ -321,7 +322,8 @@ await User.objects().filter(name__regex=r"^\d{3}").exec() # SurrealDB's
|
|
|
321
322
|
- **`match` and `regex`.** Both take a regular expression in SurrealDB's syntax (the Rust
|
|
322
323
|
`regex` crate) and match anywhere in the value unless it is anchored (`^…$`). Prefix it
|
|
323
324
|
with `(?i)` to ignore case. Here `match` is a regex lookup, not the full SurrealDB-ORM's
|
|
324
|
-
full-text `@@` operator: full-text search is
|
|
325
|
+
full-text `@@` operator: full-text search is the separate `search()` API
|
|
326
|
+
([section 36](#36-full-text-search-search-searchscore-searchhighlight-analyzer-index)).
|
|
325
327
|
- **Values.** The value must be a `str`, otherwise `ValueError`. A string starting with `$`
|
|
326
328
|
is data, as for every lookup ([String filter values and `Var()`](#string-filter-values-and-var)).
|
|
327
329
|
`startswith`, `endswith`, `match` and `regex` also take a `Var`. `like` and `ilike` do not:
|
|
@@ -5833,6 +5835,298 @@ Applying needs a privileged session: run `define_table()` at start-up or deploym
|
|
|
5833
5835
|
(or `TYPE ANY`) for any model with `table_type`; generated models carry the keys
|
|
5834
5836
|
([section 31](#31-schema-introspection-databaseintrospector-generate_models_from_db)).
|
|
5835
5837
|
|
|
5838
|
+
### 36. Full-text search (`search()`, `SearchScore`, `SearchHighlight`, `Analyzer`, `Index`)
|
|
5839
|
+
|
|
5840
|
+
Since v0.35.0 a queryset searches text with the full SurrealDB-ORM's API — `search()`, with `SearchScore` and
|
|
5841
|
+
`SearchHighlight` in `annotate()` — plus `SearchOffsets`, over the full-text indexes a model declares with `Analyzer`
|
|
5842
|
+
and `Index` and applies with `define_indexes()`. SurrealDB tokenises, matches, scores (BM25) and highlights on the
|
|
5843
|
+
server; the ORM binds every search text and highlight tag as a query variable. The same code runs on SurrealDB 2.x,
|
|
5844
|
+
3.x and the embedded engine, except where the [2.x/3.x table](#orm-behaviour-surrealdb-2x-vs-3x) says otherwise. The
|
|
5845
|
+
examples assume a configured connection: they run as written, in order, on the embedded engine (`mem://`).
|
|
5846
|
+
|
|
5847
|
+
#### Declare and apply
|
|
5848
|
+
|
|
5849
|
+
An `Analyzer` is SurrealDB's `DEFINE ANALYZER`, with the full ORM's `DefineAnalyzer` arguments: a name, tokenizers
|
|
5850
|
+
(`blank`, `camel`, `class`, `punct`), filters (`ascii`, `lowercase`, `uppercase`, `ngram(min,max)`,
|
|
5851
|
+
`edgengram(min,max)`, `snowball(language)`, `mapper('path')`), and optionally `function=` and `comment=`. An `Index`
|
|
5852
|
+
is `DEFINE INDEX`, with `CreateIndex`'s arguments: a plain index, a `UNIQUE` one (`unique=True`), or a full-text one
|
|
5853
|
+
(`search_analyzer=`, an `Analyzer` or the name of an analyzer the database defines), which covers one field and takes
|
|
5854
|
+
`bm25=` (`True`, or a `(k1, b)` pair) and `highlights=True`. Each argument is checked when the object is built. A
|
|
5855
|
+
model lists its indexes in `SurrealConfigDict(indexes=[...])`, checked when the class is defined (a list or a tuple of
|
|
5856
|
+
`Index`, distinct names, one full-text index per column, one `Analyzer` per analyzer name); `get_indexes()` returns
|
|
5857
|
+
them in declaration order.
|
|
5858
|
+
|
|
5859
|
+
```python
|
|
5860
|
+
from surreal_orm_lite import (
|
|
5861
|
+
Analyzer,
|
|
5862
|
+
BaseSurrealModel,
|
|
5863
|
+
Count,
|
|
5864
|
+
Index,
|
|
5865
|
+
SearchHighlight,
|
|
5866
|
+
SearchOffsets,
|
|
5867
|
+
SearchScore,
|
|
5868
|
+
Subquery,
|
|
5869
|
+
SurrealConfigDict,
|
|
5870
|
+
SurrealDbError,
|
|
5871
|
+
)
|
|
5872
|
+
|
|
5873
|
+
ENGLISH = Analyzer("english", ["class", "blank"], ["lowercase", "ascii", "snowball(english)"])
|
|
5874
|
+
|
|
5875
|
+
|
|
5876
|
+
class Article(BaseSurrealModel):
|
|
5877
|
+
model_config = SurrealConfigDict(
|
|
5878
|
+
table_name="article",
|
|
5879
|
+
indexes=[
|
|
5880
|
+
Index("article_title", "title", search_analyzer=ENGLISH, bm25=True, highlights=True),
|
|
5881
|
+
Index("article_body", "body", search_analyzer=ENGLISH),
|
|
5882
|
+
Index("article_slug", "slug", unique=True),
|
|
5883
|
+
],
|
|
5884
|
+
)
|
|
5885
|
+
|
|
5886
|
+
id: str | None = None
|
|
5887
|
+
title: str
|
|
5888
|
+
body: str = ""
|
|
5889
|
+
slug: str
|
|
5890
|
+
topic: str = ""
|
|
5891
|
+
|
|
5892
|
+
|
|
5893
|
+
print(*Article.index_ddl(), sep="\n")
|
|
5894
|
+
# DEFINE ANALYZER OVERWRITE `english` TOKENIZERS class,blank FILTERS lowercase,ascii,snowball(english);
|
|
5895
|
+
# DEFINE INDEX OVERWRITE `article_title` ON `article` FIELDS `title` FULLTEXT ANALYZER `english` BM25 HIGHLIGHTS;
|
|
5896
|
+
# DEFINE INDEX OVERWRITE `article_body` ON `article` FIELDS `body` FULLTEXT ANALYZER `english`;
|
|
5897
|
+
# DEFINE INDEX OVERWRITE `article_slug` ON `article` FIELDS `slug` UNIQUE;
|
|
5898
|
+
await Article.define_table() # the table first
|
|
5899
|
+
applied = await Article.define_indexes()
|
|
5900
|
+
print(applied == Article.index_ddl(line=2)) # the embedded engine is a SurrealDB 2.x core: SEARCH
|
|
5901
|
+
# True
|
|
5902
|
+
print(await Article.define_indexes()) # nothing changed, nothing applied
|
|
5903
|
+
# []
|
|
5904
|
+
```
|
|
5905
|
+
|
|
5906
|
+
`define_indexes()` runs on the model's connection. It reads what the database holds (`INFO FOR DB`, `INFO FOR TABLE`)
|
|
5907
|
+
and the line (`client.version()`, when a full-text index is declared), then applies only what is missing or changed,
|
|
5908
|
+
one statement per request, and returns the statements applied: on an unchanged database it sends the reads alone and
|
|
5909
|
+
returns `[]`, so it is safe at start-up. It writes the line's keyword: `FULLTEXT` on SurrealDB 3.x, `SEARCH` on 2.x and
|
|
5910
|
+
the embedded engine, each of which refuses the other's. `overwrite=False` defines what is missing (`IF NOT EXISTS`) and
|
|
5911
|
+
leaves a changed index as it is. `index_ddl()` renders the statements without a database, for SurrealDB 3.x, or for 2.x
|
|
5912
|
+
with `index_ddl(line=2)` (a script for a 2.x server). Nothing is removed: `remove_index()` removes an index by name
|
|
5913
|
+
(`Article.remove_index("article_body")` sends `REMOVE INDEX IF EXISTS`; with `missing_ok=False`, a missing index
|
|
5914
|
+
raises `SurrealDbNotFoundError`). An analyzer an index names by `str` must exist, or `define_indexes()` raises
|
|
5915
|
+
`SurrealDbError` before any DDL: `SurrealDBConnectionManager.define_analyzer()` defines one on the current connection
|
|
5916
|
+
(redefining it only when it changed, and returning the statements applied), and `remove_analyzer()` removes one by
|
|
5917
|
+
name, refusing it while an index uses it. `define_indexes()` and `define_analyzer()` drop every cached read
|
|
5918
|
+
([section 29](#29-query-cache-cache-querycache)), `remove_index()` the model's.
|
|
5919
|
+
|
|
5920
|
+
Call `define_table()` (or `define_relation()` for an edge model) before `define_indexes()`: on a table that does not
|
|
5921
|
+
exist yet, SurrealDB's `DEFINE INDEX` creates it `TYPE ANY SCHEMALESS PERMISSIONS NONE`, which `define_table()`
|
|
5922
|
+
replaces but `define_table(overwrite=False)` leaves as it is.
|
|
5923
|
+
|
|
5924
|
+
#### Search, score, highlight
|
|
5925
|
+
|
|
5926
|
+
`search(**field_queries)` keeps the records whose fields match: each keyword names a field — a Python field name (an
|
|
5927
|
+
aliased field searches its column) or a dotted path (`**{"meta.headline": "rust"}`) — and the text to match, which is
|
|
5928
|
+
bound, never written into the statement, and is data whatever it holds. Each field gets the next match reference, in
|
|
5929
|
+
call order and across calls (`search(title="a").search(body="b")`: references 0 and 1), which `SearchScore(ref)`,
|
|
5930
|
+
`SearchHighlight(open_tag, close_tag, ref)` and `SearchOffsets(ref)` name in `annotate()`: the BM25 score, a `float`;
|
|
5931
|
+
the text with each matched term between the tags, a `str` (a list of strings for an array field); where the terms sit,
|
|
5932
|
+
SurrealDB's `{"<item>": [{"s": <start>, "e": <end>}]}`. `exec()`, `first()` and `get()` without an id set each value
|
|
5933
|
+
as an attribute named after its alias, never written back (`model_dump()`, `save()` and `update()` leave it out); rows
|
|
5934
|
+
that do not validate (a `select()` leaving out a required field) come back as dicts holding the aliases.
|
|
5935
|
+
`order_by("-relevance")` sorts on an alias. An alias that would shadow a field, its column or an attribute of the
|
|
5936
|
+
model raises `ValueError`, as does a reference no `search()` gave, before any request.
|
|
5937
|
+
|
|
5938
|
+
```python
|
|
5939
|
+
corpus = [
|
|
5940
|
+
("a1", "Quantum physics for beginners", "An introduction to quantum mechanics", "science"),
|
|
5941
|
+
("a2", "Cooking with physics", "Heat and physics in the kitchen", "food"),
|
|
5942
|
+
("a3", "Gardening in spring", "Plants and soil", "garden"),
|
|
5943
|
+
("a4", "Quantum computing explained", "Qubits and gates", "science"),
|
|
5944
|
+
("a5", "Baking bread at home", "Flour water salt", "food"),
|
|
5945
|
+
("a6", "Sewing for beginners", "Needles and thread", "craft"),
|
|
5946
|
+
]
|
|
5947
|
+
for key, title, body, topic in corpus:
|
|
5948
|
+
await Article(id=key, title=title, body=body, slug=key, topic=topic).save()
|
|
5949
|
+
|
|
5950
|
+
articles = await (
|
|
5951
|
+
Article.objects()
|
|
5952
|
+
.search(title="physics")
|
|
5953
|
+
.annotate(relevance=SearchScore(0), snippet=SearchHighlight("<b>", "</b>", 0), spans=SearchOffsets(0))
|
|
5954
|
+
.order_by("-relevance")
|
|
5955
|
+
.exec()
|
|
5956
|
+
)
|
|
5957
|
+
for article in articles:
|
|
5958
|
+
print(article.id, round(article.relevance, 3), article.snippet, article.spans)
|
|
5959
|
+
# a2 0.613 Cooking with <b>physics</b> {'0': [{'e': 20, 's': 13}]}
|
|
5960
|
+
# a1 0.543 Quantum <b>physics</b> for beginners {'0': [{'e': 15, 's': 8}]}
|
|
5961
|
+
```
|
|
5962
|
+
|
|
5963
|
+
`search()` applies wherever `filter()` does, AND-ed after `filter()`, `Q` and `nearby()`: `count()`, `exists()`, the
|
|
5964
|
+
aggregations, `values()` groups, `bulk_update()`, `bulk_delete()`, `patch()`, and a `Subquery` of the queryset
|
|
5965
|
+
([section 28](#28-subqueries-subquery)), whose references start again at 0. `get(<id>)` and `all()` ignore it, as they
|
|
5966
|
+
ignore `filter()`.
|
|
5967
|
+
|
|
5968
|
+
```python
|
|
5969
|
+
quantum_qubits = Article.objects().search(title="quantum", body="qubits")
|
|
5970
|
+
both = await quantum_qubits.annotate(t=SearchScore(0), b=SearchScore(1)).exec()
|
|
5971
|
+
print([(article.id, article.t > 0, article.b > 0) for article in both])
|
|
5972
|
+
# [('a4', True, True)]
|
|
5973
|
+
print(await Article.objects().search(title="quantum").count())
|
|
5974
|
+
# 2
|
|
5975
|
+
print(await Article.objects().filter(topic="food").search(title="physics").exists())
|
|
5976
|
+
# True
|
|
5977
|
+
print(await Article.objects().search(title="beginners").values("topic").annotate(n=Count()).exec())
|
|
5978
|
+
# [{'n': 1, 'topic': 'craft'}, {'n': 1, 'topic': 'science'}]
|
|
5979
|
+
beginners = Subquery(Article.objects().search(title="beginners"))
|
|
5980
|
+
print(await Article.objects().filter(id__in=beginners).search(body="thread").count())
|
|
5981
|
+
# 1
|
|
5982
|
+
print(await Article.objects().search(body="kitchen").bulk_update(topic="cooking"))
|
|
5983
|
+
# 1
|
|
5984
|
+
```
|
|
5985
|
+
|
|
5986
|
+
#### What SurrealDB decides
|
|
5987
|
+
|
|
5988
|
+
- The analyzer decides what matches: its tokenizers split the text and its filters normalise each token — case,
|
|
5989
|
+
accents, stems (`snowball`), prefixes (`edgengram`) — and the search text goes through the same analyzer.
|
|
5990
|
+
- Within a field, the terms of one text are AND-ed: `search(title="quantum physics")` keeps the records whose title
|
|
5991
|
+
holds both; an empty text matches nothing, and so does a field that holds no text (`None`, a number).
|
|
5992
|
+
- BM25 scores depend on the line: SurrealDB 3.x gives a term found in more than half the documents no weight (a score
|
|
5993
|
+
of `0.0`) and keeps its statistics in step with the records, while 2.x and the embedded engine score such a term
|
|
5994
|
+
below zero, count an index's records again each time it is rebuilt, and keep counting deleted records. Compare
|
|
5995
|
+
scores within one query, and upgrade to SurrealDB 3.x where scores must not depend on the index's history.
|
|
5996
|
+
- `SearchHighlight` and `SearchOffsets` need the index's `highlights=True` (`HIGHLIGHTS`): without it SurrealDB returns
|
|
5997
|
+
the text unhighlighted, and no offsets (`None`).
|
|
5998
|
+
- A search on a field without a full-text index raises `SurrealDbError` naming the field. The server's own error,
|
|
5999
|
+
which on SurrealDB 3.x quotes the search text, is never shown.
|
|
6000
|
+
|
|
6001
|
+
```python
|
|
6002
|
+
print([article.id for article in await Article.objects().search(title="quantum physics").exec()])
|
|
6003
|
+
# ['a1']
|
|
6004
|
+
print(await Article.objects().search(title="").count())
|
|
6005
|
+
# 0
|
|
6006
|
+
try:
|
|
6007
|
+
await Article.objects().search(slug="a1").exec()
|
|
6008
|
+
except SurrealDbError as error:
|
|
6009
|
+
print(error)
|
|
6010
|
+
# search(): no full-text index on field 'slug'. SurrealDB matches only a field with a FULLTEXT (3.x) or SEARCH (2.x) index: declare Index(..., search_analyzer=...) on the model and apply define_indexes().
|
|
6011
|
+
```
|
|
6012
|
+
|
|
6013
|
+
#### Changing an analyzer
|
|
6014
|
+
|
|
6015
|
+
SurrealDB keeps the tokens of the records an index holds when their analyzer changes: a search tokenised the new way
|
|
6016
|
+
misses them — the records written afterwards use the new analyzer — until the index is rebuilt. So when
|
|
6017
|
+
`define_indexes()`, or `SurrealDBConnectionManager.define_analyzer()`, redefines an analyzer, it rebuilds every index of
|
|
6018
|
+
the database that uses it (`REBUILD INDEX`, sorted by table then index; `define_indexes()` rebuilds the model's table's
|
|
6019
|
+
last), and logs one warning naming them (logger `surreal_orm_lite.search`). A rebuild re-reads the table: seconds
|
|
6020
|
+
per thousands of records on SurrealDB 2.x and the embedded engine. An analyzer belongs to the database, not to a table:
|
|
6021
|
+
declare one `Analyzer` per name (a module constant) and reference it from every index that uses it — two definitions
|
|
6022
|
+
under one name would redefine it in turn, each time rebuilding its indexes. An unchanged index is never redefined, so
|
|
6023
|
+
start-up never rebuilds (and, on SurrealDB 2.x, never inflates its scores).
|
|
6024
|
+
|
|
6025
|
+
```python
|
|
6026
|
+
PLAIN = Analyzer("plain", ["blank"], ["ascii"])
|
|
6027
|
+
|
|
6028
|
+
|
|
6029
|
+
class Note(BaseSurrealModel):
|
|
6030
|
+
model_config = SurrealConfigDict(table_name="note", indexes=[Index("note_text", "text", search_analyzer=PLAIN)])
|
|
6031
|
+
|
|
6032
|
+
id: str | None = None
|
|
6033
|
+
text: str
|
|
6034
|
+
|
|
6035
|
+
|
|
6036
|
+
await Note.define_table()
|
|
6037
|
+
await Note.define_indexes()
|
|
6038
|
+
await Note(id="n1", text="Physics Rocks").save()
|
|
6039
|
+
print(await Note.objects().search(text="physics").count()) # ascii keeps the case: "Physics" is not "physics"
|
|
6040
|
+
# 0
|
|
6041
|
+
|
|
6042
|
+
LOWERED = Analyzer("plain", ["blank"], ["lowercase", "ascii"]) # the same analyzer, changed in a later release
|
|
6043
|
+
|
|
6044
|
+
|
|
6045
|
+
class NoteV2(Note):
|
|
6046
|
+
model_config = SurrealConfigDict(table_name="note", indexes=[Index("note_text", "text", search_analyzer=LOWERED)])
|
|
6047
|
+
|
|
6048
|
+
|
|
6049
|
+
print(*await NoteV2.define_indexes(), sep="\n") # logs: Analyzer 'plain' redefined; 1 index(es) rebuilt: note.note_text
|
|
6050
|
+
# DEFINE ANALYZER OVERWRITE `plain` TOKENIZERS blank FILTERS lowercase,ascii;
|
|
6051
|
+
# REBUILD INDEX `note_text` ON `note`;
|
|
6052
|
+
print(await NoteV2.objects().search(text="physics").count())
|
|
6053
|
+
# 1
|
|
6054
|
+
```
|
|
6055
|
+
|
|
6056
|
+
#### Transactions
|
|
6057
|
+
|
|
6058
|
+
- `define_indexes(tx=…)` reads first — committed state, outside the transaction — then its index statements join it
|
|
6059
|
+
([section 11](#11-transactions-atomic-all-or-nothing)): applied at once in an interactive transaction (WebSocket,
|
|
6060
|
+
SurrealDB 3.x), at the commit in a buffered one (HTTP, SurrealDB 2.x, the embedded engine), where a refusal comes
|
|
6061
|
+
back as the transaction's own `SurrealDbError`. A block that raises applies nothing. `remove_index(tx=…)` joins it
|
|
6062
|
+
the same way.
|
|
6063
|
+
- The analyzers must be in place: SurrealDB builds an index with the analyzers committed before the transaction began.
|
|
6064
|
+
So `define_indexes(tx=…)` raises `SurrealDbError` before any DDL when an analyzer must be defined or redefined, and
|
|
6065
|
+
`define_analyzer(tx=…)` when a redefinition has indexes to rebuild (a new analyzer, or a redefinition no index uses,
|
|
6066
|
+
joins the transaction): call `define_analyzer()` without `tx=` first.
|
|
6067
|
+
- Prefer `define_indexes()` outside the block: in a transaction that also writes to the model's table, SurrealDB may
|
|
6068
|
+
leave out of the index a record the block wrote before the index was defined, and may fail the block on a write
|
|
6069
|
+
after it.
|
|
6070
|
+
- `remove_analyzer(tx=…)` reads committed state to find the indexes that use the analyzer: an index queued earlier in
|
|
6071
|
+
a buffered block is not seen.
|
|
6072
|
+
- A search in an interactive transaction sees the transaction's writes; in a buffered one, a read raises, as every
|
|
6073
|
+
read does there.
|
|
6074
|
+
- In a transaction, a search on a field without a full-text index raises the same `SurrealDbError`, which the
|
|
6075
|
+
transaction records: the block's commit then fails with it as its cause, and neither error quotes the search text.
|
|
6076
|
+
|
|
6077
|
+
#### Live queries
|
|
6078
|
+
|
|
6079
|
+
- `live()` and `watch()` refuse `search()` — and a search annotation — before any request (`SurrealDbError`):
|
|
6080
|
+
SurrealDB accepts a live query whose condition holds a full-text match, but never notifies it
|
|
6081
|
+
([section 21](#21-live-queries-real-time-notifications)).
|
|
6082
|
+
- Search in a subquery instead:
|
|
6083
|
+
`Article.objects().filter(id__in=Subquery(Article.objects().search(title="physics"))).live()` notifies the records
|
|
6084
|
+
created or updated to match — not a deletion, nor an update that makes a record stop matching.
|
|
6085
|
+
- Before a live query whose subquery searches, the ORM checks that each searched field has a full-text index and
|
|
6086
|
+
raises `SurrealDbError` naming the field otherwise, before the live query starts: without the index, SurrealDB 3.x
|
|
6087
|
+
would accept the live query and never notify. Do not remove that index while the live query runs.
|
|
6088
|
+
|
|
6089
|
+
#### Not supported
|
|
6090
|
+
|
|
6091
|
+
- `as_of()` ([section 27](#27-time-travel-as_of)): `search()` with `as_of()` raises `SurrealDbError` before any
|
|
6092
|
+
request, in either order and in a subquery under it. The index holds the records' current text, so a time-travel
|
|
6093
|
+
read would select records by their text now.
|
|
6094
|
+
- `changes()` ([section 22](#22-change-feeds-changes)): a change feed refuses `search()` and the search annotations
|
|
6095
|
+
(`SurrealDbError`), and a `Subquery` refuses the search annotations.
|
|
6096
|
+
- Indexes on materialized views ([section 33](#33-materialized-views-view_query-define_view)), refused with
|
|
6097
|
+
`ValueError` when the class is defined. A materialized view model cannot declare `indexes=`: SurrealDB 3.x does not
|
|
6098
|
+
index the records a view gains after the index is defined, and `define_view()` drops the indexes of the view's table.
|
|
6099
|
+
- `@AND@`/`@OR@`: SurrealDB 3.x's boolean match operators are not exposed. The terms of one text, and the fields of
|
|
6100
|
+
`search()`, are AND-ed; there is no OR of two matches.
|
|
6101
|
+
|
|
6102
|
+
#### Coming from the full SurrealDB-ORM
|
|
6103
|
+
|
|
6104
|
+
- **Declared on the model**: `Analyzer` and `Index` take the argument names of the full ORM's `DefineAnalyzer` and
|
|
6105
|
+
`CreateIndex`, a model lists its indexes in `SurrealConfigDict(indexes=[...])` and `define_indexes()` applies them;
|
|
6106
|
+
the full ORM declares analyzers and indexes in migrations, which this library adds in v0.38.0.
|
|
6107
|
+
- **`search()` applies wherever `filter()` applies**: `count()`, the aggregations, `values()` groups, `exists()`,
|
|
6108
|
+
`bulk_update()`, `bulk_delete()`, `patch()` and a `Subquery`; the full ORM applies it to `exec()` only.
|
|
6109
|
+
- **Refused where SurrealDB cannot honour it**: `search()` with `live()`/`watch()`, `as_of()` or `changes()` raises
|
|
6110
|
+
before any request; the full ORM's `live()` ignores it.
|
|
6111
|
+
- **The highlight tags are bound query variables**, and `SearchScore`, `SearchHighlight` and `SearchOffsets` have no
|
|
6112
|
+
public `to_surql()` (the full ORM writes the tags into the statement, escaping quotes only).
|
|
6113
|
+
- **`SearchOffsets` is an addition** of this library: SurrealDB's `search::offsets()`, where the matched terms sit in
|
|
6114
|
+
the field.
|
|
6115
|
+
- **A field without a full-text index raises `SurrealDbError` naming the field**, instead of the server's error,
|
|
6116
|
+
which on SurrealDB 3.x quotes the search text.
|
|
6117
|
+
- **Checked before any request**: the text to match must be a `str`, every match reference an annotation names must
|
|
6118
|
+
come from `search()`, and `Index`/`Analyzer` arguments are checked at construction, more strictly than the server
|
|
6119
|
+
where its answer would be silently wrong (BM25 parameters out of range, an n-gram whose minimum exceeds its maximum,
|
|
6120
|
+
`UNIQUE` on a full-text index).
|
|
6121
|
+
- **The index keyword follows the server line**: `FULLTEXT` on SurrealDB 3.x, `SEARCH` on 2.x and the embedded engine;
|
|
6122
|
+
the full ORM writes `FULLTEXT` only, which 2.x refuses.
|
|
6123
|
+
- **A `COMMENT` is escaped with a backslash**; the full ORM's doubled quote is a parse error on both lines.
|
|
6124
|
+
- **`Index` has no `concurrently` argument and no vector arguments yet**: it covers plain, `UNIQUE` and full-text
|
|
6125
|
+
indexes; vector indexes come in v0.36.0.
|
|
6126
|
+
- **`filter(x__match=...)` stays a regular expression**: this library's `match` lookup, not SurrealDB's `@@`;
|
|
6127
|
+
`search()` is the full-text path.
|
|
6128
|
+
- **`hybrid_search()` comes in v0.37.0.**
|
|
6129
|
+
|
|
5836
6130
|
---
|
|
5837
6131
|
|
|
5838
6132
|
## Configuration Options
|
|
@@ -5955,7 +6249,7 @@ features introduced in SurrealDB 3.x. On 2.x the ORM degrades gracefully. Capabi
|
|
|
5955
6249
|
listed behave the same on both lines. The 2.x column was measured on 2.6.5 and holds unchanged
|
|
5956
6250
|
on 2.7.0. The 3.x column was measured on 3.2.4 and holds on 3.3.0, except where a row says
|
|
5957
6251
|
otherwise. The full suite and every divergence below were re-verified on both new versions.
|
|
5958
|
-
Rows added in v0.20.0 were measured directly on 2.7.0 and 3.3.0; rows added in v0.21.0 on 2.7.0 and 3.3.2, re-verified on 3.3.3, with 3.1.5 where it differs; rows added in v0.22.0 on 2.7.0 and 3.3.3, with 3.1.5 where it differs; rows added in v0.23.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.24.0 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.26.0 on 2.7.0 and 3.3.4, in memory and on versioned storage, identical on 3.1.5; rows added in v0.27.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.28.0 on 2.7.0 and 3.3.4, identical on 3.1.5; the rows added in v0.28.1 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.29.0 on 2.7.0 and 3.3.4 in one process, identical on 3.1.5. The row added in v0.29.2 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.30.0 were measured on 2.7.0, 3.3.4 and the embedded engine, with 3.1.5 where it differs. The row added in v0.30.3 was measured on 2.7.0 and 3.3.4, over WebSocket and HTTP, and on the embedded engine, identical on 3.1.5. The row added in v0.30.7 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5: its parser refuses a statement that nests six objects, and `update()` nests at most four. Rows added in v0.31.0 were measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. The row added in v0.31.1 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.32.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4 but accepts raw writes to a view. Rows added in v0.33.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4. Rows added in v0.34.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4 except that it refuses a Python `set` written to a `set` field, and an object item of an optional untyped `list`.
|
|
6252
|
+
Rows added in v0.20.0 were measured directly on 2.7.0 and 3.3.0; rows added in v0.21.0 on 2.7.0 and 3.3.2, re-verified on 3.3.3, with 3.1.5 where it differs; rows added in v0.22.0 on 2.7.0 and 3.3.3, with 3.1.5 where it differs; rows added in v0.23.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.24.0 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.26.0 on 2.7.0 and 3.3.4, in memory and on versioned storage, identical on 3.1.5; rows added in v0.27.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.28.0 on 2.7.0 and 3.3.4, identical on 3.1.5; the rows added in v0.28.1 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.29.0 on 2.7.0 and 3.3.4 in one process, identical on 3.1.5. The row added in v0.29.2 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.30.0 were measured on 2.7.0, 3.3.4 and the embedded engine, with 3.1.5 where it differs. The row added in v0.30.3 was measured on 2.7.0 and 3.3.4, over WebSocket and HTTP, and on the embedded engine, identical on 3.1.5. The row added in v0.30.7 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5: its parser refuses a statement that nests six objects, and `update()` nests at most four. Rows added in v0.31.0 were measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. The row added in v0.31.1 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.32.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4 but accepts raw writes to a view. Rows added in v0.33.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4. Rows added in v0.34.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4 except that it refuses a Python `set` written to a `set` field, and an object item of an optional untyped `list`. Rows added in v0.35.0 were measured on 2.7.0, 3.3.4 and the embedded engine, with 3.1.5 where it differs.
|
|
5959
6253
|
The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded engine](#orm-behaviour-the-embedded-engine).
|
|
5960
6254
|
|
|
5961
6255
|
| ORM capability | SurrealDB 2.6.x / 2.7.x | SurrealDB 3.2.x / 3.3.x | Since |
|
|
@@ -6098,6 +6392,13 @@ The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded
|
|
|
6098
6392
|
| An `Any` or untyped `list`/`set` field of a `SCHEMAFULL` model | `define_table()` adds `FLEXIBLE` (read from `client.version()`): without it these engines empty such values, optional or not; the item definitions as on 3.x | kept without `FLEXIBLE` (refused there); an untyped `list` gets an item definition `TYPE any`, without which 3.x refuses an object item of a required one (`no such field exists`) | v0.34.0 |
|
|
6099
6393
|
| A `list` of sub-models (`list[M]`, at any depth) holding a `dict`, an `Any` or an untyped `list`, in a `SCHEMAFULL` model | `define_table()` also writes the `list` `FLEXIBLE`: without it these engines drop such a value from every item, then refuse the record's next write (`Found NONE for field`) | kept without `FLEXIBLE` on the `list`, which `define_table()` does not write there | v0.34.0 |
|
|
6100
6394
|
| A write naming a field a `SCHEMAFULL` table does not define (an undeclared `merge()` key, a raw query, records predating `SCHEMAFULL`) | the field is dropped, silently; kept in the items of a list `define_table()` writes `FLEXIBLE` (a 2.x limitation) | the write is refused (`no such field exists`), except in the items of a list of objects, which keep it | v0.34.0 |
|
|
6395
|
+
| Full-text index (`define_indexes()`, `index_ddl(line=)`) | `SEARCH ANALYZER` (`FULLTEXT` is a parse error), read from `client.version()`; several fields accepted, the ORM writes one | `FULLTEXT ANALYZER` (`SEARCH` is a parse error); one field per index | v0.35.0 |
|
|
6396
|
+
| Removing an analyzer an index uses (`remove_analyzer()`) | the server would accept it, then refuse every write to the table: the ORM refuses it first, with 3.x's message | refused by the server | v0.35.0 |
|
|
6397
|
+
| BM25 score of a term in more than half the documents (`SearchScore`) | below zero: a text holding such a term scores lower than without it, and ranks differently than on 3.x | `0.0`: the term adds nothing to the score | v0.35.0 |
|
|
6398
|
+
| BM25 statistics after a rebuild, a redefinition or a deletion (`SearchScore`) | not reset: a `REBUILD INDEX`, an `OVERWRITE` or a new index of the same name counts the records again, and deleted records stay counted; the matches are unaffected | kept in step with the records: the scores of a freshly built index | v0.35.0 |
|
|
6399
|
+
| Two `search()` fields AND-ed | only the records matching every field, each field scored as if searched alone | the same on 3.3; 3.1 can also return records that match only one of the fields, scored `0.0` on the other | v0.35.0 |
|
|
6400
|
+
| `search()` on a field without a full-text index | `SurrealDbError` naming the field | the same; 3.1 answers no record | v0.35.0 |
|
|
6401
|
+
| `live()` with a `Subquery` that searches a field without a full-text index | `SurrealDbError` naming the field, before the live query starts | the same (the live query would never notify); 3.1 starts it | v0.35.0 |
|
|
6101
6402
|
| Full suite and every divergence in this table, re-run on SurrealDB 2.7.0 and 3.3.0 | 2.7.0 identical to 2.6.5 (same passes, same 3.x-only skips) | 3.3.0 identical to 3.2.4, except the system-user signin row | v0.19.1 |
|
|
6102
6403
|
|
|
6103
6404
|
> **Note on record IDs**: A record loaded from the database has its `id` field set to a native `surrealdb.RecordID` object, not a plain string. Use `model.get_raw_id()` to obtain the bare identifier string (e.g. `"alice"`), or compare directly with `model.id == RecordID("User", "alice")`. In-memory instances you construct yourself retain whatever value you assign.
|
|
@@ -6134,6 +6435,7 @@ Capabilities not listed behave as on a 2.x server.
|
|
|
6134
6435
|
| Materialized views (v0.32.0) | as on a 2.x server: `int` sums and `Decimal` means; in a grouped view, `math::stddev()`/`math::variance()`, an expression around an aggregate, a field outside `GROUP BY` and a `GROUP BY` on an alias raise `SurrealDbError` before any DDL; writes to a view refused by the ORM (the engine would accept them); no live queries | `float` sums and means on 3.x | v0.32.0 |
|
|
6135
6436
|
| TYPE RELATION constraints (v0.33.0) | as on a 2.x server: a raw `ALTER TABLE … TYPE` of a table already written to is not enforced (`define_relation()` sends `DEFINE TABLE OVERWRITE`); an endpoint outside `IN`/`OUT` whose record does not exist raises `SurrealDbNotFoundError` | `SurrealDbError` for that endpoint on 3.x | v0.33.0 |
|
|
6136
6437
|
| Model table DDL (v0.34.0) | as on a 2.x server: `define_table()` adds `FLEXIBLE` to the `Any` and untyped columns of a `SCHEMAFULL` table and to its lists of sub-models holding a `FLEXIBLE` column; a write naming a field the table does not define drops that field | 3.x keeps those values without `FLEXIBLE` and refuses that write | v0.34.0 |
|
|
6438
|
+
| Full-text search (v0.35.0) | as on a 2.x server: `SEARCH ANALYZER`, a term in more than half the documents scores below zero, BM25 statistics grow with each rebuild and keep deleted records, `remove_analyzer()` refuses an analyzer an index uses; an index defined in the transaction that defines its analyzer sees it, which `define_indexes(tx=…)` refuses all the same | 3.x writes `FULLTEXT ANALYZER`, scores such a term `0.0`, keeps BM25 statistics exact and refuses that removal | v0.35.0 |
|
|
6137
6439
|
| Everything else | CRUD, QuerySet, aggregations, relations, typed edges, rich and geo fields, upsert/patch, server values, computed fields, stored functions, signals: the same as on a 2.x server | unchanged | v0.25.0 |
|
|
6138
6440
|
|
|
6139
6441
|
---
|
|
@@ -6152,40 +6454,41 @@ Contributions are welcome! Please:
|
|
|
6152
6454
|
|
|
6153
6455
|
## Roadmap
|
|
6154
6456
|
|
|
6155
|
-
| Version | Theme
|
|
6156
|
-
| ----------------- |
|
|
6157
|
-
| v0.2.x – v0.7.0 | Core ORM → SDK 2.0 / SurrealDB 3.x migration
|
|
6158
|
-
| v0.8.0 | Transactions ORM (`tx=`)
|
|
6159
|
-
| v0.9.0 | Transactions — QuerySet & interactive (3.x)
|
|
6160
|
-
| v0.10.0 | upsert / update_or_create / get_or_create
|
|
6161
|
-
| v0.11.0 | patch / atomic field & array ops
|
|
6162
|
-
| v0.12.0 | retry_on_conflict & optimistic concurrency
|
|
6163
|
-
| v0.13.0 | SurrealFunc & server-side values
|
|
6164
|
-
| v0.14.0 | Computed fields (`DEFINE FIELD … VALUE`)
|
|
6165
|
-
| v0.14.3 – v0.14.5 | Correctness: `$`-values, record-id lookups, loops
|
|
6166
|
-
| v0.15.0 | `call_function()` — custom `fn::` stored functions
|
|
6167
|
-
| v0.16.0 | Connection auth (`signin`/`signup`/`info`)
|
|
6168
|
-
| v0.17.0 | Model auth (`AuthenticatedUserMixin`)
|
|
6169
|
-
| v0.18.0 | Field aliases, `server_fields`, `merge(refresh=)`
|
|
6170
|
-
| v0.19.0 | Live queries (base): `live()` / `kill()`, raw notifs
|
|
6171
|
-
| v0.20.0 | Typed live queries: `LiveModelStream`, diff, filters
|
|
6172
|
-
| v0.21.0 | Auto-resubscribe & change feeds (`changes()`)
|
|
6173
|
-
| v0.22.0 | Tier 1 — Native typed relations
|
|
6174
|
-
| v0.23.0 | Rich field types
|
|
6175
|
-
| v0.24.0 | Geospatial fields
|
|
6176
|
-
| v0.25.0 | Embedded test engine
|
|
6177
|
-
| v0.26.0 | Time travel (versioned storage)
|
|
6178
|
-
| v0.27.0 | Subqueries
|
|
6179
|
-
| v0.28.0 | Query cache
|
|
6180
|
-
| v0.29.0 | Tier 2 — Multi-database
|
|
6181
|
-
| v0.30.0 | Tier 3 — Schema introspection
|
|
6182
|
-
| v0.31.0 | Tier 3 — Server-side events and DDL helpers
|
|
6183
|
-
| v0.32.0 | Tier 3 — Materialized views
|
|
6184
|
-
| v0.33.0 | Tier 3 — `TYPE RELATION` constraints
|
|
6185
|
-
| v0.34.0 | Tier 3 — Model table DDL
|
|
6186
|
-
| v0.35.0
|
|
6187
|
-
| v0.
|
|
6188
|
-
|
|
|
6457
|
+
| Version | Theme | Status |
|
|
6458
|
+
| ----------------- | ------------------------------------------------------------- | ----------- |
|
|
6459
|
+
| v0.2.x – v0.7.0 | Core ORM → SDK 2.0 / SurrealDB 3.x migration | ✅ Released |
|
|
6460
|
+
| v0.8.0 | Transactions ORM (`tx=`) | ✅ Released |
|
|
6461
|
+
| v0.9.0 | Transactions — QuerySet & interactive (3.x) | ✅ Released |
|
|
6462
|
+
| v0.10.0 | upsert / update_or_create / get_or_create | ✅ Released |
|
|
6463
|
+
| v0.11.0 | patch / atomic field & array ops | ✅ Released |
|
|
6464
|
+
| v0.12.0 | retry_on_conflict & optimistic concurrency | ✅ Released |
|
|
6465
|
+
| v0.13.0 | SurrealFunc & server-side values | ✅ Released |
|
|
6466
|
+
| v0.14.0 | Computed fields (`DEFINE FIELD … VALUE`) | ✅ Released |
|
|
6467
|
+
| v0.14.3 – v0.14.5 | Correctness: `$`-values, record-id lookups, loops | ✅ Released |
|
|
6468
|
+
| v0.15.0 | `call_function()` — custom `fn::` stored functions | ✅ Released |
|
|
6469
|
+
| v0.16.0 | Connection auth (`signin`/`signup`/`info`) | ✅ Released |
|
|
6470
|
+
| v0.17.0 | Model auth (`AuthenticatedUserMixin`) | ✅ Released |
|
|
6471
|
+
| v0.18.0 | Field aliases, `server_fields`, `merge(refresh=)` | ✅ Released |
|
|
6472
|
+
| v0.19.0 | Live queries (base): `live()` / `kill()`, raw notifs | ✅ Released |
|
|
6473
|
+
| v0.20.0 | Typed live queries: `LiveModelStream`, diff, filters | ✅ Released |
|
|
6474
|
+
| v0.21.0 | Auto-resubscribe & change feeds (`changes()`) | ✅ Released |
|
|
6475
|
+
| v0.22.0 | Tier 1 — Native typed relations | ✅ Released |
|
|
6476
|
+
| v0.23.0 | Rich field types | ✅ Released |
|
|
6477
|
+
| v0.24.0 | Geospatial fields | ✅ Released |
|
|
6478
|
+
| v0.25.0 | Embedded test engine | ✅ Released |
|
|
6479
|
+
| v0.26.0 | Time travel (versioned storage) | ✅ Released |
|
|
6480
|
+
| v0.27.0 | Subqueries | ✅ Released |
|
|
6481
|
+
| v0.28.0 | Query cache | ✅ Released |
|
|
6482
|
+
| v0.29.0 | Tier 2 — Multi-database | ✅ Released |
|
|
6483
|
+
| v0.30.0 | Tier 3 — Schema introspection | ✅ Released |
|
|
6484
|
+
| v0.31.0 | Tier 3 — Server-side events and DDL helpers | ✅ Released |
|
|
6485
|
+
| v0.32.0 | Tier 3 — Materialized views | ✅ Released |
|
|
6486
|
+
| v0.33.0 | Tier 3 — `TYPE RELATION` constraints | ✅ Released |
|
|
6487
|
+
| v0.34.0 | Tier 3 — Model table DDL | ✅ Released |
|
|
6488
|
+
| v0.35.0 | Tier 3 — Full-text search | ✅ Released |
|
|
6489
|
+
| v0.36.0 – v0.40.0 | Tier 3 — Advanced (vector and hybrid search, migrations, CLI) | 📋 Planned |
|
|
6490
|
+
| v0.41.0 | Beta Phase (API freeze, hardening) | 📋 Planned |
|
|
6491
|
+
| v2.0.0 | Production / GA (aligned with SDK 2.0) | 📋 Planned |
|
|
6189
6492
|
|
|
6190
6493
|
> Every roadmap feature is implementable with the **official SDK 2.0** (native methods or
|
|
6191
6494
|
> `query()` SurrealQL) — no custom SDK. GA is numbered **v2.0.0** to mirror SDK 2.0; the `1.x`
|
|
@@ -6242,7 +6545,7 @@ SDK) and **server support**. Everything below is on the lite roadmap via the off
|
|
|
6242
6545
|
| Materialized views | ✅ v0.32.0 (`view_query`, `define_view()`) | ✅ |
|
|
6243
6546
|
| TYPE RELATION enforcement | ✅ v0.33.0 (`relation_in`, `define_relation()`) | ✅ DDL (`TableType.RELATION`, migrations) |
|
|
6244
6547
|
| Model table DDL | ✅ v0.34.0 (`define_table()`) | ✅ |
|
|
6245
|
-
| Full-Text Search | v0.35.0
|
|
6548
|
+
| Full-Text Search | ✅ v0.35.0 (`search()`) | ✅ |
|
|
6246
6549
|
| Vector Search (KNN/HNSW) | v0.36.0 | ✅ |
|
|
6247
6550
|
| Hybrid Search (RRF) | v0.37.0 | ✅ |
|
|
6248
6551
|
| Migrations & CLI | v0.38 – v0.39 | ✅ |
|