onyx-database 2.3.0__tar.gz → 2.4.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.
- {onyx_database-2.3.0 → onyx_database-2.4.0}/CHANGELOG.md +19 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/PKG-INFO +98 -9
- {onyx_database-2.3.0 → onyx_database-2.4.0}/README.md +97 -8
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/__init__.py +6 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/candidate_search.py +111 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/conditions.py +57 -5
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/onyx.py +65 -11
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/onyx_async.py +65 -11
- onyx_database-2.4.0/onyx_database/py.typed +1 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_builder.py +113 -14
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_builder_async.py +53 -8
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/types.py +20 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/pyproject.toml +1 -1
- {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/bump-version.sh +1 -1
- onyx_database-2.4.0/tests/test_search.py +526 -0
- onyx_database-2.3.0/tests/test_search.py +0 -93
- {onyx_database-2.3.0 → onyx_database-2.4.0}/.github/workflows/ci.yml +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/.github/workflows/publish.yml +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/.gitignore +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/CONTRIBUTING.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/LICENSE +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/README.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/__init__.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_shorthand.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_shorthand_stream.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_stream.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/models.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/script_approval.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/streaming.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/delete/by_id.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/delete/query.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/document/save_get_delete_document.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/aggregate_avg.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/aggregates_with_grouping.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/basic.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/basic_inference.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/compound.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/find_by_id.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/first_or_none.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/format.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/full_text_search_all_tables.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/full_text_table_search.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/in_partition.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/inner_query.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/list.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/not_inner_query.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/order_by.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/resolver.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/resolver_with_hints.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/search_by_resolver_fields.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/select_example.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/sorting_and_paging.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/update.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/basic.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/batch_save.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/cascade.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/cascade_builder.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/schema/basic.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/secrets/basic.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/seed.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/close.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/create_events.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/delete_events.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/query_stream.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/update_events.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx.schema.json +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/ai.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/config.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/entity_wire.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/errors.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/__init__.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/aggregates.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/sort.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/http.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_results.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_results_async.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/stream.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/plan/acceptance-criteria.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/plan/plan.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/schema/onyx.schema.json +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/release-flow.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/run-examples.sh +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/01-http-resilience.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/02-async-client.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/03-query-values.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/04-codegen-pydantic.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/05-cli-parity.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/06-packaging-ci.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/07-onyx-cli-python-exports.md +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/fixtures/entity-wire-v1.json +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/fixtures/entity-wire-v1.msgpack.hex +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_ai.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_async.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_candidate_search.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_entity_wire.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_http.py +0 -0
- {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_query_results.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 2.4.0 - 2026-08-29
|
|
6
|
+
|
|
7
|
+
- Added one high-level `search` API for lexical, semantic, and hybrid retrieval
|
|
8
|
+
across sync and async table builders and database-wide facades. The new form
|
|
9
|
+
accepts an options mapping or Python keyword arguments, emits the fail-closed
|
|
10
|
+
`SEARCH` wire operator, defaults to hybrid/any retrieval, validates scores in
|
|
11
|
+
`0..1`, and remains composable with structured filters while rejecting
|
|
12
|
+
mutations.
|
|
13
|
+
- Preserved the exact legacy `MATCHES` wire shape for `search(text)` and
|
|
14
|
+
`search(text, min_score)` calls.
|
|
15
|
+
- Added typed `SearchOptions` overloads and a PEP 561 marker while retaining the
|
|
16
|
+
legacy numeric-score overloads on sync and async builders and database-wide
|
|
17
|
+
facades.
|
|
18
|
+
- Added recursive guards for duplicate or mis-targeted `SEARCH`, mixed
|
|
19
|
+
`__full_text__` predicates, and unsupported live query streams. Database-wide
|
|
20
|
+
search no longer inherits a configured default partition.
|
|
21
|
+
|
|
3
22
|
## 2.3.0 - 2026-08-29
|
|
4
23
|
|
|
5
24
|
- Added typed, hard-bounded native `CANDIDATES`, `SEARCH_CANDIDATES`, and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: onyx-database
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Python client SDK for Onyx Cloud Database (builder-pattern API and schema helpers)
|
|
5
5
|
Project-URL: Homepage, https://onyx.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/OnyxDevTools/onyx-database-python
|
|
@@ -199,7 +199,7 @@ db = onyx.init(
|
|
|
199
199
|
|
|
200
200
|
#### Default partition + logging
|
|
201
201
|
|
|
202
|
-
- `partition` sets a default partition for queries, `find_by_id`, and deletes by primary key.
|
|
202
|
+
- `partition` sets a default partition for table-scoped queries, `find_by_id`, and deletes by primary key; database-wide `db.search(...)` omits it.
|
|
203
203
|
- Save operations use the partition field on the entity itself (if present).
|
|
204
204
|
- `request_logging_enabled` logs HTTP requests and JSON bodies.
|
|
205
205
|
- `response_logging_enabled` logs HTTP responses and JSON bodies.
|
|
@@ -503,7 +503,30 @@ from onyx_database import (
|
|
|
503
503
|
- Prefer `within` / `not_within` for inclusion checks (supports arrays, comma-separated strings, or inner queries).
|
|
504
504
|
- `in_op` / `not_in` remain available for backward compatibility and are exact aliases.
|
|
505
505
|
|
|
506
|
-
|
|
506
|
+
For application search, choose lexical, semantic, or hybrid mode directly:
|
|
507
|
+
|
|
508
|
+
```py
|
|
509
|
+
question = "how do i calculate cost per horse"
|
|
510
|
+
|
|
511
|
+
lexical = db.from_table("ActiveDocumentChunk").search(
|
|
512
|
+
question,
|
|
513
|
+
mode="lexical",
|
|
514
|
+
match="any",
|
|
515
|
+
).list()
|
|
516
|
+
|
|
517
|
+
semantic = db.from_table("ActiveDocumentChunk").search(
|
|
518
|
+
question,
|
|
519
|
+
mode="semantic",
|
|
520
|
+
).list()
|
|
521
|
+
|
|
522
|
+
hybrid = db.from_table("ActiveDocumentChunk").search(
|
|
523
|
+
question,
|
|
524
|
+
mode="hybrid",
|
|
525
|
+
).list()
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Native candidate channels remain available as explicit, physically bounded
|
|
529
|
+
low-level APIs:
|
|
507
530
|
|
|
508
531
|
```py
|
|
509
532
|
from onyx_database import hnsw_search_query
|
|
@@ -587,9 +610,75 @@ roles_missing_permission = (
|
|
|
587
610
|
)
|
|
588
611
|
```
|
|
589
612
|
|
|
590
|
-
###
|
|
613
|
+
### Lexical, semantic, and hybrid search
|
|
614
|
+
|
|
615
|
+
The high-level API sends natural-language text to the database when you pass an
|
|
616
|
+
options mapping or search-option keywords. `mode` defaults to `"hybrid"`, `match`
|
|
617
|
+
defaults to `"any"`, `min_score` defaults to `None`, and `max_candidates` defaults
|
|
618
|
+
to `1000`.
|
|
619
|
+
|
|
620
|
+
```py
|
|
621
|
+
question = "how do i calculate cost per horse"
|
|
622
|
+
|
|
623
|
+
# Match any normalized query term.
|
|
624
|
+
lexical = db.from_table(tables.ActiveDocumentChunk).search(
|
|
625
|
+
question,
|
|
626
|
+
mode="lexical",
|
|
627
|
+
match="any",
|
|
628
|
+
min_score=0.4,
|
|
629
|
+
max_candidates=500,
|
|
630
|
+
).list()
|
|
631
|
+
|
|
632
|
+
# Let the database embed the query and use semantic retrieval.
|
|
633
|
+
semantic = db.from_table(tables.ActiveDocumentChunk).search(
|
|
634
|
+
question,
|
|
635
|
+
mode="semantic",
|
|
636
|
+
).list()
|
|
637
|
+
|
|
638
|
+
# Combine lexical and semantic retrieval.
|
|
639
|
+
hybrid = db.from_table(tables.ActiveDocumentChunk).search(
|
|
640
|
+
question,
|
|
641
|
+
mode="hybrid",
|
|
642
|
+
).list()
|
|
643
|
+
|
|
644
|
+
# A mapping is equivalent; Python snake_case and wire camelCase are accepted.
|
|
645
|
+
same_hybrid = db.from_table(tables.ActiveDocumentChunk).search(
|
|
646
|
+
question,
|
|
647
|
+
{"mode": "hybrid", "match": "any", "max_candidates": 500},
|
|
648
|
+
).list()
|
|
649
|
+
```
|
|
591
650
|
|
|
592
|
-
|
|
651
|
+
The same options work through `db.search(...)` for database-wide search and on
|
|
652
|
+
the async client. When supplied, `mode` must be `"lexical"`, `"semantic"`, or
|
|
653
|
+
`"hybrid"`; `match` must be `"all"` or `"any"`; `min_score` must be finite and
|
|
654
|
+
between 0 and 1; and `max_candidates` must be between 1 and 5000. Hybrid mode
|
|
655
|
+
requires at least 2 candidates so both lexical and semantic channels receive a
|
|
656
|
+
candidate budget.
|
|
657
|
+
High-level `SEARCH` queries are read-only but may be combined with structured
|
|
658
|
+
filters using `where`, `and_`, or `or_`. A query may contain only one `SEARCH`,
|
|
659
|
+
it cannot contain another `__full_text__` predicate, and high-level/candidate
|
|
660
|
+
search plans cannot be used for live query streams.
|
|
661
|
+
|
|
662
|
+
Table-scoped high-level search spans the table's current partitions by default
|
|
663
|
+
under one global candidate budget; use `in_partition(...)` to constrain it.
|
|
664
|
+
The low-level candidate APIs below still require one concrete partition.
|
|
665
|
+
Database-wide `db.search(...)` searches eligible unpartitioned tables only and
|
|
666
|
+
does not inherit the client's configured default partition.
|
|
667
|
+
|
|
668
|
+
Semantic and hybrid modes require a server-side embedding provider. The provider
|
|
669
|
+
must embed saved searchable text and query text with the same model and vector
|
|
670
|
+
space; marking a field searchable does not select or configure that provider.
|
|
671
|
+
Rows saved before embedding was enabled must be explicitly resaved or backfilled
|
|
672
|
+
before HNSW can retrieve them.
|
|
673
|
+
|
|
674
|
+
### Legacy vector-managed search
|
|
675
|
+
|
|
676
|
+
Use `.search(...)` on a builder or the `search` predicate helper to add a `MATCHES` condition against the `__full_text__` pseudo-field. `db.search(...)` sets `table = "ALL"` and seeds a query builder with that condition. Paging remains a query parameter; partition is deliberately omitted from `ALL` searches.
|
|
677
|
+
|
|
678
|
+
For compatibility, calls with no options and calls whose only second argument is
|
|
679
|
+
a numeric score retain this legacy wire contract. Pass an options mapping or a
|
|
680
|
+
high-level option such as `mode`, `match`, or `max_candidates` to opt into
|
|
681
|
+
`SEARCH`.
|
|
593
682
|
|
|
594
683
|
```py
|
|
595
684
|
from onyx_database import onyx, search, eq
|
|
@@ -727,7 +816,7 @@ Example request bodies emitted by the SDK:
|
|
|
727
816
|
}
|
|
728
817
|
```
|
|
729
818
|
|
|
730
|
-
For
|
|
819
|
+
For advanced caller-supplied semantic routing, build a validated
|
|
731
820
|
`VectorSearchQuery`. The helper accepts Python snake_case arguments and emits the
|
|
732
821
|
Cloud API's camelCase wire fields:
|
|
733
822
|
|
|
@@ -817,8 +906,8 @@ validation are available on `AsyncQueryBuilder`.
|
|
|
817
906
|
Condition forms are exported as `approximate_search`, `hnsw_candidates`, and
|
|
818
907
|
`approximate_candidates`. Use them only as the sole condition, for example
|
|
819
908
|
`.where(approximate_search("storm", max_candidates=250))`. Database-wide
|
|
820
|
-
`db.search(...)`
|
|
821
|
-
|
|
909
|
+
`db.search(...)` accepts the same high-level mode, match, score, and candidate
|
|
910
|
+
options as a table builder.
|
|
822
911
|
|
|
823
912
|
---
|
|
824
913
|
|
|
@@ -1111,7 +1200,7 @@ except (OnyxConfigError, OnyxHTTPError) as err:
|
|
|
1111
1200
|
|
|
1112
1201
|
A typical release flow for this repository:
|
|
1113
1202
|
|
|
1114
|
-
1. Update the version in `
|
|
1203
|
+
1. Update the project version in `pyproject.toml`.
|
|
1115
1204
|
2. Build: `python -m build`
|
|
1116
1205
|
3. Publish: `twine upload dist/*`
|
|
1117
1206
|
|
|
@@ -174,7 +174,7 @@ db = onyx.init(
|
|
|
174
174
|
|
|
175
175
|
#### Default partition + logging
|
|
176
176
|
|
|
177
|
-
- `partition` sets a default partition for queries, `find_by_id`, and deletes by primary key.
|
|
177
|
+
- `partition` sets a default partition for table-scoped queries, `find_by_id`, and deletes by primary key; database-wide `db.search(...)` omits it.
|
|
178
178
|
- Save operations use the partition field on the entity itself (if present).
|
|
179
179
|
- `request_logging_enabled` logs HTTP requests and JSON bodies.
|
|
180
180
|
- `response_logging_enabled` logs HTTP responses and JSON bodies.
|
|
@@ -478,7 +478,30 @@ from onyx_database import (
|
|
|
478
478
|
- Prefer `within` / `not_within` for inclusion checks (supports arrays, comma-separated strings, or inner queries).
|
|
479
479
|
- `in_op` / `not_in` remain available for backward compatibility and are exact aliases.
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
For application search, choose lexical, semantic, or hybrid mode directly:
|
|
482
|
+
|
|
483
|
+
```py
|
|
484
|
+
question = "how do i calculate cost per horse"
|
|
485
|
+
|
|
486
|
+
lexical = db.from_table("ActiveDocumentChunk").search(
|
|
487
|
+
question,
|
|
488
|
+
mode="lexical",
|
|
489
|
+
match="any",
|
|
490
|
+
).list()
|
|
491
|
+
|
|
492
|
+
semantic = db.from_table("ActiveDocumentChunk").search(
|
|
493
|
+
question,
|
|
494
|
+
mode="semantic",
|
|
495
|
+
).list()
|
|
496
|
+
|
|
497
|
+
hybrid = db.from_table("ActiveDocumentChunk").search(
|
|
498
|
+
question,
|
|
499
|
+
mode="hybrid",
|
|
500
|
+
).list()
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Native candidate channels remain available as explicit, physically bounded
|
|
504
|
+
low-level APIs:
|
|
482
505
|
|
|
483
506
|
```py
|
|
484
507
|
from onyx_database import hnsw_search_query
|
|
@@ -562,9 +585,75 @@ roles_missing_permission = (
|
|
|
562
585
|
)
|
|
563
586
|
```
|
|
564
587
|
|
|
565
|
-
###
|
|
588
|
+
### Lexical, semantic, and hybrid search
|
|
589
|
+
|
|
590
|
+
The high-level API sends natural-language text to the database when you pass an
|
|
591
|
+
options mapping or search-option keywords. `mode` defaults to `"hybrid"`, `match`
|
|
592
|
+
defaults to `"any"`, `min_score` defaults to `None`, and `max_candidates` defaults
|
|
593
|
+
to `1000`.
|
|
594
|
+
|
|
595
|
+
```py
|
|
596
|
+
question = "how do i calculate cost per horse"
|
|
597
|
+
|
|
598
|
+
# Match any normalized query term.
|
|
599
|
+
lexical = db.from_table(tables.ActiveDocumentChunk).search(
|
|
600
|
+
question,
|
|
601
|
+
mode="lexical",
|
|
602
|
+
match="any",
|
|
603
|
+
min_score=0.4,
|
|
604
|
+
max_candidates=500,
|
|
605
|
+
).list()
|
|
606
|
+
|
|
607
|
+
# Let the database embed the query and use semantic retrieval.
|
|
608
|
+
semantic = db.from_table(tables.ActiveDocumentChunk).search(
|
|
609
|
+
question,
|
|
610
|
+
mode="semantic",
|
|
611
|
+
).list()
|
|
612
|
+
|
|
613
|
+
# Combine lexical and semantic retrieval.
|
|
614
|
+
hybrid = db.from_table(tables.ActiveDocumentChunk).search(
|
|
615
|
+
question,
|
|
616
|
+
mode="hybrid",
|
|
617
|
+
).list()
|
|
618
|
+
|
|
619
|
+
# A mapping is equivalent; Python snake_case and wire camelCase are accepted.
|
|
620
|
+
same_hybrid = db.from_table(tables.ActiveDocumentChunk).search(
|
|
621
|
+
question,
|
|
622
|
+
{"mode": "hybrid", "match": "any", "max_candidates": 500},
|
|
623
|
+
).list()
|
|
624
|
+
```
|
|
566
625
|
|
|
567
|
-
|
|
626
|
+
The same options work through `db.search(...)` for database-wide search and on
|
|
627
|
+
the async client. When supplied, `mode` must be `"lexical"`, `"semantic"`, or
|
|
628
|
+
`"hybrid"`; `match` must be `"all"` or `"any"`; `min_score` must be finite and
|
|
629
|
+
between 0 and 1; and `max_candidates` must be between 1 and 5000. Hybrid mode
|
|
630
|
+
requires at least 2 candidates so both lexical and semantic channels receive a
|
|
631
|
+
candidate budget.
|
|
632
|
+
High-level `SEARCH` queries are read-only but may be combined with structured
|
|
633
|
+
filters using `where`, `and_`, or `or_`. A query may contain only one `SEARCH`,
|
|
634
|
+
it cannot contain another `__full_text__` predicate, and high-level/candidate
|
|
635
|
+
search plans cannot be used for live query streams.
|
|
636
|
+
|
|
637
|
+
Table-scoped high-level search spans the table's current partitions by default
|
|
638
|
+
under one global candidate budget; use `in_partition(...)` to constrain it.
|
|
639
|
+
The low-level candidate APIs below still require one concrete partition.
|
|
640
|
+
Database-wide `db.search(...)` searches eligible unpartitioned tables only and
|
|
641
|
+
does not inherit the client's configured default partition.
|
|
642
|
+
|
|
643
|
+
Semantic and hybrid modes require a server-side embedding provider. The provider
|
|
644
|
+
must embed saved searchable text and query text with the same model and vector
|
|
645
|
+
space; marking a field searchable does not select or configure that provider.
|
|
646
|
+
Rows saved before embedding was enabled must be explicitly resaved or backfilled
|
|
647
|
+
before HNSW can retrieve them.
|
|
648
|
+
|
|
649
|
+
### Legacy vector-managed search
|
|
650
|
+
|
|
651
|
+
Use `.search(...)` on a builder or the `search` predicate helper to add a `MATCHES` condition against the `__full_text__` pseudo-field. `db.search(...)` sets `table = "ALL"` and seeds a query builder with that condition. Paging remains a query parameter; partition is deliberately omitted from `ALL` searches.
|
|
652
|
+
|
|
653
|
+
For compatibility, calls with no options and calls whose only second argument is
|
|
654
|
+
a numeric score retain this legacy wire contract. Pass an options mapping or a
|
|
655
|
+
high-level option such as `mode`, `match`, or `max_candidates` to opt into
|
|
656
|
+
`SEARCH`.
|
|
568
657
|
|
|
569
658
|
```py
|
|
570
659
|
from onyx_database import onyx, search, eq
|
|
@@ -702,7 +791,7 @@ Example request bodies emitted by the SDK:
|
|
|
702
791
|
}
|
|
703
792
|
```
|
|
704
793
|
|
|
705
|
-
For
|
|
794
|
+
For advanced caller-supplied semantic routing, build a validated
|
|
706
795
|
`VectorSearchQuery`. The helper accepts Python snake_case arguments and emits the
|
|
707
796
|
Cloud API's camelCase wire fields:
|
|
708
797
|
|
|
@@ -792,8 +881,8 @@ validation are available on `AsyncQueryBuilder`.
|
|
|
792
881
|
Condition forms are exported as `approximate_search`, `hnsw_candidates`, and
|
|
793
882
|
`approximate_candidates`. Use them only as the sole condition, for example
|
|
794
883
|
`.where(approximate_search("storm", max_candidates=250))`. Database-wide
|
|
795
|
-
`db.search(...)`
|
|
796
|
-
|
|
884
|
+
`db.search(...)` accepts the same high-level mode, match, score, and candidate
|
|
885
|
+
options as a table builder.
|
|
797
886
|
|
|
798
887
|
---
|
|
799
888
|
|
|
@@ -1086,7 +1175,7 @@ except (OnyxConfigError, OnyxHTTPError) as err:
|
|
|
1086
1175
|
|
|
1087
1176
|
A typical release flow for this repository:
|
|
1088
1177
|
|
|
1089
|
-
1. Update the version in `
|
|
1178
|
+
1. Update the project version in `pyproject.toml`.
|
|
1090
1179
|
2. Build: `python -m build`
|
|
1091
1180
|
3. Publish: `twine upload dist/*`
|
|
1092
1181
|
|
|
@@ -66,6 +66,9 @@ from .types import (
|
|
|
66
66
|
HnswSearchQueryInput,
|
|
67
67
|
Int64WireInput,
|
|
68
68
|
QueryCriteriaOperator,
|
|
69
|
+
SearchMatch,
|
|
70
|
+
SearchMode,
|
|
71
|
+
SearchOptions,
|
|
69
72
|
SemanticVectorSignature,
|
|
70
73
|
SemanticVectorSignatureInput,
|
|
71
74
|
VectorSearchQuery,
|
|
@@ -152,6 +155,9 @@ __all__ = [
|
|
|
152
155
|
"HnswSearchQueryInput",
|
|
153
156
|
"Int64WireInput",
|
|
154
157
|
"QueryCriteriaOperator",
|
|
158
|
+
"SearchMatch",
|
|
159
|
+
"SearchMode",
|
|
160
|
+
"SearchOptions",
|
|
155
161
|
"SemanticVectorSignature",
|
|
156
162
|
"SemanticVectorSignatureInput",
|
|
157
163
|
"VectorSearchQuery",
|
|
@@ -13,6 +13,8 @@ from ..types import (
|
|
|
13
13
|
ApproximateIndexCandidateQuery,
|
|
14
14
|
HnswSearchQuery,
|
|
15
15
|
Int64WireInput,
|
|
16
|
+
SearchMatch,
|
|
17
|
+
SearchMode,
|
|
16
18
|
SemanticVectorSignature,
|
|
17
19
|
VectorSearchQuery,
|
|
18
20
|
)
|
|
@@ -37,6 +39,16 @@ _SEMANTIC_BAND_COUNT = 4
|
|
|
37
39
|
_MISSING = object()
|
|
38
40
|
_DECIMAL_RE = re.compile(r"^-?\d+$")
|
|
39
41
|
_HEX_RE = re.compile(r"^[0-9a-fA-F]+$")
|
|
42
|
+
_SEARCH_MODES = {"lexical", "semantic", "hybrid"}
|
|
43
|
+
_SEARCH_MATCH_POLICIES = {"all", "any"}
|
|
44
|
+
_SEARCH_OPTION_FIELDS = {
|
|
45
|
+
"mode",
|
|
46
|
+
"match",
|
|
47
|
+
"minScore",
|
|
48
|
+
"min_score",
|
|
49
|
+
"maxCandidates",
|
|
50
|
+
"max_candidates",
|
|
51
|
+
}
|
|
40
52
|
|
|
41
53
|
|
|
42
54
|
def _mapping(input_value: Mapping[str, Any] | None, field: str) -> Mapping[str, Any]:
|
|
@@ -62,6 +74,105 @@ def _value(
|
|
|
62
74
|
return default
|
|
63
75
|
|
|
64
76
|
|
|
77
|
+
def _aliased_search_option(
|
|
78
|
+
values: Mapping[str, Any],
|
|
79
|
+
wire_name: str,
|
|
80
|
+
snake_name: str,
|
|
81
|
+
*,
|
|
82
|
+
default: Any = _MISSING,
|
|
83
|
+
) -> Any:
|
|
84
|
+
present = [name for name in (wire_name, snake_name) if name in values]
|
|
85
|
+
if len(present) > 1:
|
|
86
|
+
raise TypeError(f"provide only one of {wire_name} and {snake_name}")
|
|
87
|
+
if present:
|
|
88
|
+
return values[present[0]]
|
|
89
|
+
if default is _MISSING:
|
|
90
|
+
raise TypeError(f"{snake_name} is required")
|
|
91
|
+
return default
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def high_level_search_query(
|
|
95
|
+
text: Any,
|
|
96
|
+
options: Mapping[str, Any] | None = None,
|
|
97
|
+
*,
|
|
98
|
+
mode: Any = _MISSING,
|
|
99
|
+
match: Any = _MISSING,
|
|
100
|
+
min_score: Any = _MISSING,
|
|
101
|
+
max_candidates: Any = _MISSING,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Validate and canonicalize a text-first lexical, semantic, or hybrid query."""
|
|
104
|
+
|
|
105
|
+
if not isinstance(text, str):
|
|
106
|
+
raise TypeError("search text must be a string")
|
|
107
|
+
if not text.strip():
|
|
108
|
+
raise ValueError("search text must not be blank")
|
|
109
|
+
|
|
110
|
+
has_options_mapping = options is not None
|
|
111
|
+
values = _mapping(options, "SearchOptions")
|
|
112
|
+
unknown = sorted(set(values) - _SEARCH_OPTION_FIELDS)
|
|
113
|
+
if unknown:
|
|
114
|
+
raise TypeError(f"unsupported search option: {unknown[0]}")
|
|
115
|
+
|
|
116
|
+
keyword_values = (mode, match, min_score, max_candidates)
|
|
117
|
+
if has_options_mapping and any(
|
|
118
|
+
value is not _MISSING for value in keyword_values
|
|
119
|
+
):
|
|
120
|
+
raise TypeError("do not combine a search options mapping with search keywords")
|
|
121
|
+
|
|
122
|
+
if has_options_mapping:
|
|
123
|
+
mode = _value(values, "mode", default="hybrid")
|
|
124
|
+
if mode is None:
|
|
125
|
+
raise TypeError("search mode must be a string")
|
|
126
|
+
match = _value(values, "match", default="any")
|
|
127
|
+
min_score = _aliased_search_option(
|
|
128
|
+
values, "minScore", "min_score", default=None
|
|
129
|
+
)
|
|
130
|
+
max_candidates = _aliased_search_option(
|
|
131
|
+
values, "maxCandidates", "max_candidates", default=1_000
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
if mode is _MISSING or mode is None:
|
|
135
|
+
mode = "hybrid"
|
|
136
|
+
if match is _MISSING:
|
|
137
|
+
match = "any"
|
|
138
|
+
if min_score is _MISSING:
|
|
139
|
+
min_score = None
|
|
140
|
+
if max_candidates is _MISSING or max_candidates is None:
|
|
141
|
+
max_candidates = 1_000
|
|
142
|
+
|
|
143
|
+
if mode is _MISSING or mode is None:
|
|
144
|
+
mode = "hybrid"
|
|
145
|
+
if not isinstance(mode, str):
|
|
146
|
+
raise TypeError("search mode must be a string")
|
|
147
|
+
if mode not in _SEARCH_MODES:
|
|
148
|
+
raise ValueError("search mode must be lexical, semantic, or hybrid")
|
|
149
|
+
if not isinstance(match, str):
|
|
150
|
+
raise TypeError("search match must be a string")
|
|
151
|
+
if match not in _SEARCH_MATCH_POLICIES:
|
|
152
|
+
raise ValueError("search match must be all or any")
|
|
153
|
+
|
|
154
|
+
canonical_min_score = None
|
|
155
|
+
if min_score is not None:
|
|
156
|
+
canonical_min_score, _ = _finite_float32(min_score, "minScore")
|
|
157
|
+
if canonical_min_score < 0 or canonical_min_score > 1:
|
|
158
|
+
raise ValueError("minScore must be between 0 and 1")
|
|
159
|
+
candidate_count = _require_integer(max_candidates, "maxCandidates")
|
|
160
|
+
if candidate_count < 1 or candidate_count > MAX_VECTOR_SEARCH_CANDIDATES:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"maxCandidates must be between 1 and {MAX_VECTOR_SEARCH_CANDIDATES}"
|
|
163
|
+
)
|
|
164
|
+
if mode == "hybrid" and candidate_count < 2:
|
|
165
|
+
raise ValueError("maxCandidates must be at least 2 for hybrid search")
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"text": text,
|
|
169
|
+
"mode": cast(SearchMode, mode),
|
|
170
|
+
"match": cast(SearchMatch, match),
|
|
171
|
+
"minScore": canonical_min_score,
|
|
172
|
+
"maxCandidates": candidate_count,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
65
176
|
def _require_integer(value: Any, field: str) -> int:
|
|
66
177
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
67
178
|
raise TypeError(f"{field} must be an integer")
|
|
@@ -5,10 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from typing import Any, Optional, Sequence, Union
|
|
7
7
|
|
|
8
|
-
from ..types import Condition, QueryBuilderLike
|
|
8
|
+
from ..types import Condition, QueryBuilderLike, SearchMatch, SearchMode, SearchOptions
|
|
9
9
|
from .candidate_search import (
|
|
10
10
|
DEFAULT_APPROXIMATE_INDEX_CANDIDATES,
|
|
11
11
|
approximate_index_candidate_query,
|
|
12
|
+
high_level_search_query,
|
|
12
13
|
hnsw_search_query,
|
|
13
14
|
vector_search_query,
|
|
14
15
|
)
|
|
@@ -123,19 +124,70 @@ def not_contains_ignore_case(field: str, value: Any) -> Condition:
|
|
|
123
124
|
|
|
124
125
|
def search(
|
|
125
126
|
query_text_or_search: Any,
|
|
126
|
-
min_score: Optional[float] = None,
|
|
127
|
+
min_score: Optional[float] | SearchOptions = None,
|
|
127
128
|
*,
|
|
129
|
+
mode: Optional[SearchMode] = None,
|
|
130
|
+
match: Optional[SearchMatch] = None,
|
|
128
131
|
semantic: Any = None,
|
|
129
132
|
nearby_bucket_radius: Optional[int] = None,
|
|
130
133
|
max_candidates: Optional[int] = None,
|
|
131
134
|
require_all_terms: Optional[bool] = None,
|
|
132
135
|
) -> Condition:
|
|
133
|
-
"""Create
|
|
136
|
+
"""Create a legacy predicate or high-level natural-language search condition.
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
A plain string, optionally followed only by a numeric score, retains the
|
|
139
|
+
established ``queryText`` wire shape. Passing a ``SearchOptions`` mapping as
|
|
140
|
+
the second argument, or passing a high-level option keyword, emits the
|
|
141
|
+
canonical ``SEARCH`` contract. First-argument mappings remain the advanced
|
|
142
|
+
``VectorSearchQuery`` path.
|
|
137
143
|
"""
|
|
138
144
|
|
|
145
|
+
options_mapping = min_score if isinstance(min_score, Mapping) else None
|
|
146
|
+
legacy_vector_options = (
|
|
147
|
+
semantic is not None
|
|
148
|
+
or nearby_bucket_radius is not None
|
|
149
|
+
or require_all_terms is not None
|
|
150
|
+
)
|
|
151
|
+
high_level_keywords = (
|
|
152
|
+
mode is not None
|
|
153
|
+
or match is not None
|
|
154
|
+
or (max_candidates is not None and not legacy_vector_options)
|
|
155
|
+
)
|
|
156
|
+
if options_mapping is not None or high_level_keywords:
|
|
157
|
+
if not isinstance(query_text_or_search, str):
|
|
158
|
+
raise TypeError("the high-level search API requires text")
|
|
159
|
+
if options_mapping is not None:
|
|
160
|
+
if (
|
|
161
|
+
high_level_keywords
|
|
162
|
+
or semantic is not None
|
|
163
|
+
or nearby_bucket_radius is not None
|
|
164
|
+
or max_candidates is not None
|
|
165
|
+
or require_all_terms is not None
|
|
166
|
+
):
|
|
167
|
+
raise TypeError(
|
|
168
|
+
"do not combine a search options mapping with search keywords"
|
|
169
|
+
)
|
|
170
|
+
search_value = high_level_search_query(
|
|
171
|
+
query_text_or_search, options_mapping
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
if (
|
|
175
|
+
semantic is not None
|
|
176
|
+
or nearby_bucket_radius is not None
|
|
177
|
+
or require_all_terms is not None
|
|
178
|
+
):
|
|
179
|
+
raise TypeError(
|
|
180
|
+
"mode/match cannot be combined with semantic routing options"
|
|
181
|
+
)
|
|
182
|
+
search_value = high_level_search_query(
|
|
183
|
+
query_text_or_search,
|
|
184
|
+
mode="hybrid" if mode is None else mode,
|
|
185
|
+
match="any" if match is None else match,
|
|
186
|
+
min_score=min_score,
|
|
187
|
+
max_candidates=1_000 if max_candidates is None else max_candidates,
|
|
188
|
+
)
|
|
189
|
+
return _condition("__full_text__", "SEARCH", search_value)
|
|
190
|
+
|
|
139
191
|
has_vector_options = (
|
|
140
192
|
semantic is not None
|
|
141
193
|
or nearby_bucket_radius is not None
|