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.
Files changed (98) hide show
  1. {onyx_database-2.3.0 → onyx_database-2.4.0}/CHANGELOG.md +19 -0
  2. {onyx_database-2.3.0 → onyx_database-2.4.0}/PKG-INFO +98 -9
  3. {onyx_database-2.3.0 → onyx_database-2.4.0}/README.md +97 -8
  4. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/__init__.py +6 -0
  5. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/candidate_search.py +111 -0
  6. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/conditions.py +57 -5
  7. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/onyx.py +65 -11
  8. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/onyx_async.py +65 -11
  9. onyx_database-2.4.0/onyx_database/py.typed +1 -0
  10. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_builder.py +113 -14
  11. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_builder_async.py +53 -8
  12. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/types.py +20 -0
  13. {onyx_database-2.3.0 → onyx_database-2.4.0}/pyproject.toml +1 -1
  14. {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/bump-version.sh +1 -1
  15. onyx_database-2.4.0/tests/test_search.py +526 -0
  16. onyx_database-2.3.0/tests/test_search.py +0 -93
  17. {onyx_database-2.3.0 → onyx_database-2.4.0}/.github/workflows/ci.yml +0 -0
  18. {onyx_database-2.3.0 → onyx_database-2.4.0}/.github/workflows/publish.yml +0 -0
  19. {onyx_database-2.3.0 → onyx_database-2.4.0}/.gitignore +0 -0
  20. {onyx_database-2.3.0 → onyx_database-2.4.0}/CONTRIBUTING.md +0 -0
  21. {onyx_database-2.3.0 → onyx_database-2.4.0}/LICENSE +0 -0
  22. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/README.md +0 -0
  23. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/__init__.py +0 -0
  24. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat.py +0 -0
  25. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_shorthand.py +0 -0
  26. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_shorthand_stream.py +0 -0
  27. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/chat_stream.py +0 -0
  28. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/models.py +0 -0
  29. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/script_approval.py +0 -0
  30. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/ai/streaming.py +0 -0
  31. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/delete/by_id.py +0 -0
  32. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/delete/query.py +0 -0
  33. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/document/save_get_delete_document.py +0 -0
  34. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/aggregate_avg.py +0 -0
  35. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/aggregates_with_grouping.py +0 -0
  36. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/basic.py +0 -0
  37. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/basic_inference.py +0 -0
  38. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/compound.py +0 -0
  39. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/find_by_id.py +0 -0
  40. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/first_or_none.py +0 -0
  41. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/format.py +0 -0
  42. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/full_text_search_all_tables.py +0 -0
  43. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/full_text_table_search.py +0 -0
  44. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/in_partition.py +0 -0
  45. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/inner_query.py +0 -0
  46. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/list.py +0 -0
  47. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/not_inner_query.py +0 -0
  48. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/order_by.py +0 -0
  49. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/resolver.py +0 -0
  50. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/resolver_with_hints.py +0 -0
  51. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/search_by_resolver_fields.py +0 -0
  52. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/select_example.py +0 -0
  53. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/sorting_and_paging.py +0 -0
  54. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/query/update.py +0 -0
  55. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/basic.py +0 -0
  56. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/batch_save.py +0 -0
  57. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/cascade.py +0 -0
  58. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/save/cascade_builder.py +0 -0
  59. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/schema/basic.py +0 -0
  60. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/secrets/basic.py +0 -0
  61. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/seed.py +0 -0
  62. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/close.py +0 -0
  63. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/create_events.py +0 -0
  64. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/delete_events.py +0 -0
  65. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/query_stream.py +0 -0
  66. {onyx_database-2.3.0 → onyx_database-2.4.0}/examples/stream/update_events.py +0 -0
  67. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx.schema.json +0 -0
  68. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/ai.py +0 -0
  69. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/config.py +0 -0
  70. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/entity_wire.py +0 -0
  71. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/errors.py +0 -0
  72. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/__init__.py +0 -0
  73. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/aggregates.py +0 -0
  74. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/helpers/sort.py +0 -0
  75. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/http.py +0 -0
  76. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_results.py +0 -0
  77. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/query_results_async.py +0 -0
  78. {onyx_database-2.3.0 → onyx_database-2.4.0}/onyx_database/stream.py +0 -0
  79. {onyx_database-2.3.0 → onyx_database-2.4.0}/plan/acceptance-criteria.md +0 -0
  80. {onyx_database-2.3.0 → onyx_database-2.4.0}/plan/plan.md +0 -0
  81. {onyx_database-2.3.0 → onyx_database-2.4.0}/schema/onyx.schema.json +0 -0
  82. {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/release-flow.md +0 -0
  83. {onyx_database-2.3.0 → onyx_database-2.4.0}/scripts/run-examples.sh +0 -0
  84. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/01-http-resilience.md +0 -0
  85. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/02-async-client.md +0 -0
  86. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/03-query-values.md +0 -0
  87. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/04-codegen-pydantic.md +0 -0
  88. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/05-cli-parity.md +0 -0
  89. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/06-packaging-ci.md +0 -0
  90. {onyx_database-2.3.0 → onyx_database-2.4.0}/tasks/07-onyx-cli-python-exports.md +0 -0
  91. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/fixtures/entity-wire-v1.json +0 -0
  92. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/fixtures/entity-wire-v1.msgpack.hex +0 -0
  93. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_ai.py +0 -0
  94. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_async.py +0 -0
  95. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_candidate_search.py +0 -0
  96. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_entity_wire.py +0 -0
  97. {onyx_database-2.3.0 → onyx_database-2.4.0}/tests/test_http.py +0 -0
  98. {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.0
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
- Native candidate channels are explicit and physically bounded:
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
- ### Native vector-managed search
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
- 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 (extras like `partition`, `pageSize`, `nextPage` remain querystring params when provided).
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 table-specific lexical, semantic, or hybrid search, build a validated
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(...)` remains the text-only API because the `ALL` route performs its own
821
- bounded lexical fan-out.
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 `onyx_database/_version.py` (or use your preferred versioning tool).
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
- Native candidate channels are explicit and physically bounded:
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
- ### Native vector-managed search
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
- 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 (extras like `partition`, `pageSize`, `nextPage` remain querystring params when provided).
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 table-specific lexical, semantic, or hybrid search, build a validated
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(...)` remains the text-only API because the `ALL` route performs its own
796
- bounded lexical fan-out.
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 `onyx_database/_version.py` (or use your preferred versioning tool).
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 an exact native full-text, semantic, or hybrid search condition.
136
+ """Create a legacy predicate or high-level natural-language search condition.
134
137
 
135
- Strings retain the established ``queryText`` wire shape. A mapping is
136
- validated and emitted as a canonical ``VectorSearchQuery``.
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