diff --git a/ext/couchbase b/ext/couchbase index 170b8c51..23f08096 160000 --- a/ext/couchbase +++ b/ext/couchbase @@ -1 +1 @@ -Subproject commit 170b8c5148a8c69c5f986028b72a82c86c13c5a5 +Subproject commit 23f080966d787b9a43b2899ca14e258a0dac8f1a diff --git a/ext/couchbase.cxx b/ext/couchbase.cxx index 9e2e2a57..425d68cc 100644 --- a/ext/couchbase.cxx +++ b/ext/couchbase.cxx @@ -6701,12 +6701,21 @@ cb_extract_search_index(VALUE index, const couchbase::core::management::search:: } static VALUE -cb_Backend_search_index_get_all(VALUE self, VALUE options) +cb_Backend_search_index_get_all(VALUE self, VALUE bucket, VALUE scope, VALUE options) { const auto& cluster = cb_backend_to_cluster(self); try { couchbase::core::operations::management::search_index_get_all_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } + cb_extract_timeout(req, options); auto barrier = std::make_shared>(); auto f = barrier->get_future(); @@ -6737,7 +6746,7 @@ cb_Backend_search_index_get_all(VALUE self, VALUE options) } static VALUE -cb_Backend_search_index_get(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_get(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -6745,6 +6754,14 @@ cb_Backend_search_index_get(VALUE self, VALUE index_name, VALUE timeout) try { couchbase::core::operations::management::search_index_get_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); auto barrier = std::make_shared>(); @@ -6772,7 +6789,7 @@ cb_Backend_search_index_get(VALUE self, VALUE index_name, VALUE timeout) } static VALUE -cb_Backend_search_index_upsert(VALUE self, VALUE index_definition, VALUE timeout) +cb_Backend_search_index_upsert(VALUE self, VALUE bucket, VALUE scope, VALUE index_definition, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -6780,6 +6797,14 @@ cb_Backend_search_index_upsert(VALUE self, VALUE index_definition, VALUE timeout try { couchbase::core::operations::management::search_index_upsert_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); VALUE index_name = rb_hash_aref(index_definition, rb_id2sym(rb_intern("name"))); @@ -6849,7 +6874,7 @@ cb_Backend_search_index_upsert(VALUE self, VALUE index_definition, VALUE timeout } static VALUE -cb_Backend_search_index_drop(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_drop(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -6857,6 +6882,14 @@ cb_Backend_search_index_drop(VALUE self, VALUE index_name, VALUE timeout) try { couchbase::core::operations::management::search_index_drop_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); auto barrier = std::make_shared>(); @@ -6884,7 +6917,7 @@ cb_Backend_search_index_drop(VALUE self, VALUE index_name, VALUE timeout) } static VALUE -cb_Backend_search_index_get_documents_count(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_get_documents_count(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -6892,6 +6925,14 @@ cb_Backend_search_index_get_documents_count(VALUE self, VALUE index_name, VALUE try { couchbase::core::operations::management::search_index_get_documents_count_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); auto barrier = std::make_shared>(); @@ -6983,7 +7024,7 @@ cb_Backend_search_get_stats(VALUE self, VALUE timeout) } static VALUE -cb_Backend_search_index_pause_ingest(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_pause_ingest(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -6991,6 +7032,14 @@ cb_Backend_search_index_pause_ingest(VALUE self, VALUE index_name, VALUE timeout try { couchbase::core::operations::management::search_index_control_ingest_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.pause = true; @@ -7020,7 +7069,7 @@ cb_Backend_search_index_pause_ingest(VALUE self, VALUE index_name, VALUE timeout } static VALUE -cb_Backend_search_index_resume_ingest(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_resume_ingest(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7028,6 +7077,14 @@ cb_Backend_search_index_resume_ingest(VALUE self, VALUE index_name, VALUE timeou try { couchbase::core::operations::management::search_index_control_ingest_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.pause = false; @@ -7057,7 +7114,7 @@ cb_Backend_search_index_resume_ingest(VALUE self, VALUE index_name, VALUE timeou } static VALUE -cb_Backend_search_index_allow_querying(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_allow_querying(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7065,6 +7122,14 @@ cb_Backend_search_index_allow_querying(VALUE self, VALUE index_name, VALUE timeo try { couchbase::core::operations::management::search_index_control_query_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.allow = true; @@ -7094,7 +7159,7 @@ cb_Backend_search_index_allow_querying(VALUE self, VALUE index_name, VALUE timeo } static VALUE -cb_Backend_search_index_disallow_querying(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_disallow_querying(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7102,6 +7167,14 @@ cb_Backend_search_index_disallow_querying(VALUE self, VALUE index_name, VALUE ti try { couchbase::core::operations::management::search_index_control_query_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.allow = false; @@ -7131,7 +7204,7 @@ cb_Backend_search_index_disallow_querying(VALUE self, VALUE index_name, VALUE ti } static VALUE -cb_Backend_search_index_freeze_plan(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_freeze_plan(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7139,6 +7212,14 @@ cb_Backend_search_index_freeze_plan(VALUE self, VALUE index_name, VALUE timeout) try { couchbase::core::operations::management::search_index_control_plan_freeze_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.freeze = true; @@ -7167,7 +7248,7 @@ cb_Backend_search_index_freeze_plan(VALUE self, VALUE index_name, VALUE timeout) } static VALUE -cb_Backend_search_index_unfreeze_plan(VALUE self, VALUE index_name, VALUE timeout) +cb_Backend_search_index_unfreeze_plan(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7175,6 +7256,14 @@ cb_Backend_search_index_unfreeze_plan(VALUE self, VALUE index_name, VALUE timeou try { couchbase::core::operations::management::search_index_control_plan_freeze_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); req.freeze = false; @@ -7204,7 +7293,7 @@ cb_Backend_search_index_unfreeze_plan(VALUE self, VALUE index_name, VALUE timeou } static VALUE -cb_Backend_search_index_analyze_document(VALUE self, VALUE index_name, VALUE encoded_document, VALUE timeout) +cb_Backend_search_index_analyze_document(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE encoded_document, VALUE timeout) { const auto& cluster = cb_backend_to_cluster(self); @@ -7213,6 +7302,14 @@ cb_Backend_search_index_analyze_document(VALUE self, VALUE index_name, VALUE enc try { couchbase::core::operations::management::search_index_analyze_document_request req{}; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } cb_extract_timeout(req, timeout); req.index_name = cb_string_new(index_name); @@ -7245,7 +7342,7 @@ cb_Backend_search_index_analyze_document(VALUE self, VALUE index_name, VALUE enc } static VALUE -cb_Backend_document_search(VALUE self, VALUE index_name, VALUE query, VALUE search_request, VALUE options) +cb_Backend_document_search(VALUE self, VALUE bucket, VALUE scope, VALUE index_name, VALUE query, VALUE search_request, VALUE options) { const auto& cluster = cb_backend_to_cluster(self); @@ -7257,6 +7354,14 @@ cb_Backend_document_search(VALUE self, VALUE index_name, VALUE query, VALUE sear try { couchbase::core::operations::search_request req; + if (!NIL_P(bucket)) { + cb_check_type(bucket, T_STRING); + req.bucket_name = cb_string_new(bucket); + } + if (!NIL_P(scope)) { + cb_check_type(scope, T_STRING); + req.scope_name = cb_string_new(scope); + } if (VALUE client_context_id = rb_hash_aref(options, rb_id2sym(rb_intern("client_context_id"))); !NIL_P(client_context_id)) { cb_check_type(client_context_id, T_STRING); req.client_context_id = cb_string_new(client_context_id); @@ -9323,7 +9428,7 @@ init_backend(VALUE mCouchbase) rb_define_method(cBackend, "document_unlock", VALUE_FUNC(cb_Backend_document_unlock), 6); rb_define_method(cBackend, "document_increment", VALUE_FUNC(cb_Backend_document_increment), 5); rb_define_method(cBackend, "document_decrement", VALUE_FUNC(cb_Backend_document_decrement), 5); - rb_define_method(cBackend, "document_search", VALUE_FUNC(cb_Backend_document_search), 4); + rb_define_method(cBackend, "document_search", VALUE_FUNC(cb_Backend_document_search), 6); rb_define_method(cBackend, "document_analytics", VALUE_FUNC(cb_Backend_document_analytics), 2); rb_define_method(cBackend, "document_view", VALUE_FUNC(cb_Backend_document_view), 5); @@ -9370,19 +9475,19 @@ init_backend(VALUE mCouchbase) rb_define_method(cBackend, "collection_query_index_build_deferred", VALUE_FUNC(cb_Backend_collection_query_index_build_deferred), 4); rb_define_method(cBackend, "search_get_stats", VALUE_FUNC(cb_Backend_search_get_stats), 1); - rb_define_method(cBackend, "search_index_get_all", VALUE_FUNC(cb_Backend_search_index_get_all), 1); - rb_define_method(cBackend, "search_index_get", VALUE_FUNC(cb_Backend_search_index_get), 2); - rb_define_method(cBackend, "search_index_upsert", VALUE_FUNC(cb_Backend_search_index_upsert), 2); - rb_define_method(cBackend, "search_index_drop", VALUE_FUNC(cb_Backend_search_index_drop), 2); + rb_define_method(cBackend, "search_index_get_all", VALUE_FUNC(cb_Backend_search_index_get_all), 3); + rb_define_method(cBackend, "search_index_get", VALUE_FUNC(cb_Backend_search_index_get), 4); + rb_define_method(cBackend, "search_index_upsert", VALUE_FUNC(cb_Backend_search_index_upsert), 4); + rb_define_method(cBackend, "search_index_drop", VALUE_FUNC(cb_Backend_search_index_drop), 4); rb_define_method(cBackend, "search_index_get_stats", VALUE_FUNC(cb_Backend_search_index_get_stats), 2); - rb_define_method(cBackend, "search_index_get_documents_count", VALUE_FUNC(cb_Backend_search_index_get_documents_count), 2); - rb_define_method(cBackend, "search_index_pause_ingest", VALUE_FUNC(cb_Backend_search_index_pause_ingest), 2); - rb_define_method(cBackend, "search_index_resume_ingest", VALUE_FUNC(cb_Backend_search_index_resume_ingest), 2); - rb_define_method(cBackend, "search_index_allow_querying", VALUE_FUNC(cb_Backend_search_index_allow_querying), 2); - rb_define_method(cBackend, "search_index_disallow_querying", VALUE_FUNC(cb_Backend_search_index_disallow_querying), 2); - rb_define_method(cBackend, "search_index_freeze_plan", VALUE_FUNC(cb_Backend_search_index_freeze_plan), 2); - rb_define_method(cBackend, "search_index_unfreeze_plan", VALUE_FUNC(cb_Backend_search_index_unfreeze_plan), 2); - rb_define_method(cBackend, "search_index_analyze_document", VALUE_FUNC(cb_Backend_search_index_analyze_document), 3); + rb_define_method(cBackend, "search_index_get_documents_count", VALUE_FUNC(cb_Backend_search_index_get_documents_count), 4); + rb_define_method(cBackend, "search_index_pause_ingest", VALUE_FUNC(cb_Backend_search_index_pause_ingest), 4); + rb_define_method(cBackend, "search_index_resume_ingest", VALUE_FUNC(cb_Backend_search_index_resume_ingest), 4); + rb_define_method(cBackend, "search_index_allow_querying", VALUE_FUNC(cb_Backend_search_index_allow_querying), 4); + rb_define_method(cBackend, "search_index_disallow_querying", VALUE_FUNC(cb_Backend_search_index_disallow_querying), 4); + rb_define_method(cBackend, "search_index_freeze_plan", VALUE_FUNC(cb_Backend_search_index_freeze_plan), 4); + rb_define_method(cBackend, "search_index_unfreeze_plan", VALUE_FUNC(cb_Backend_search_index_unfreeze_plan), 4); + rb_define_method(cBackend, "search_index_analyze_document", VALUE_FUNC(cb_Backend_search_index_analyze_document), 5); rb_define_method(cBackend, "analytics_get_pending_mutations", VALUE_FUNC(cb_Backend_analytics_get_pending_mutations), 1); rb_define_method(cBackend, "analytics_dataverse_drop", VALUE_FUNC(cb_Backend_analytics_dataverse_drop), 2); diff --git a/lib/couchbase/cluster.rb b/lib/couchbase/cluster.rb index 4baf86b8..c8c37f5e 100644 --- a/lib/couchbase/cluster.rb +++ b/lib/couchbase/cluster.rb @@ -187,7 +187,7 @@ def analytics_query(statement, options = Options::Analytics::DEFAULT) # # @return [SearchResult] def search_query(index_name, query, options = Options::Search::DEFAULT) - resp = @backend.document_search(index_name, JSON.generate(query), {}, options.to_backend) + resp = @backend.document_search(nil, nil, index_name, JSON.generate(query), {}, options.to_backend) convert_search_result(resp, options) end @@ -202,7 +202,7 @@ def search_query(index_name, query, options = Options::Search::DEFAULT) # @return [SearchResult] def search(index_name, search_request, options = Options::Search::DEFAULT) encoded_query, encoded_req = search_request.to_backend - resp = @backend.document_search(index_name, encoded_query, encoded_req, options.to_backend(show_request: false)) + resp = @backend.document_search(nil, nil, index_name, encoded_query, encoded_req, options.to_backend(show_request: false)) convert_search_result(resp, options) end diff --git a/lib/couchbase/management.rb b/lib/couchbase/management.rb index 4901ff84..1dbc362e 100644 --- a/lib/couchbase/management.rb +++ b/lib/couchbase/management.rb @@ -24,5 +24,6 @@ module Management require "couchbase/management/query_index_manager" require "couchbase/management/collection_query_index_manager" require "couchbase/management/search_index_manager" +require "couchbase/management/scope_search_index_manager" require "couchbase/management/user_manager" require "couchbase/management/view_index_manager" diff --git a/lib/couchbase/management/scope_search_index_manager.rb b/lib/couchbase/management/scope_search_index_manager.rb new file mode 100644 index 00000000..7406a80c --- /dev/null +++ b/lib/couchbase/management/scope_search_index_manager.rb @@ -0,0 +1,199 @@ +# Copyright 2024. Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Couchbase + module Management + # @api volatile + class ScopeSearchIndexManager + alias inspect to_s + + # @param [Couchbase::Backend] backend + # @param [String] bucket_name + # @param [String] scope_name + def initialize(backend, bucket_name, scope_name) + @backend = backend + @bucket_name = bucket_name + @scope_name = scope_name + end + + # Fetches an index from the server if it exists + # + # @param [String] index_name name of the index + # @param [GetIndexOptions] options + # + # @return [SearchIndex] + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def get_index(index_name, options = GetIndexOptions.new) + res = @backend.search_index_get(@bucket_name, @scope_name, index_name, options.timeout) + SearchIndexManager.extract_search_index(res) + end + + # Fetches all indexes from the server + # + # @param [GetAllIndexesOptions] options + # + # @return [Array] + def get_all_indexes(options = GetAllIndexesOptions.new) + res = @backend.search_index_get_all(@bucket_name, @scope_name, options.timeout) + res[:indexes].map { |idx| SearchIndexManager.extract_search_index(idx) } + end + + # Creates or updates the index + # + # @param [SearchIndex] index_definition the index definition + # @param [UpsertIndexOptions] options + # + # @return void + # + # @raise [ArgumentError] if name, type or source_type is empty + def upsert_index(index_definition, options = UpsertIndexOptions.new) + @backend.search_index_upsert(@bucket_name, @scope_name, index_definition.to_backend, options.timeout) + end + + # Drops the index + # + # @param [String] index_name name of the index + # @param [DropIndexOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def drop_index(index_name, options = DropIndexOptions.new) + @backend.search_index_drop(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Retrieves the number of documents that have been indexed for an index + # + # @param [String] index_name name of the index + # @param [GetIndexedDocumentsCountOptions] options + # + # @return [Integer] + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def get_indexed_documents_count(index_name, options = GetIndexedDocumentsCountOptions.new) + res = @backend.search_index_get_documents_count(@bucket_name, @scope_name, index_name, options.timeout) + res[:count] + end + + # Pauses updates and maintenance for the index + # + # @param [String] index_name name of the index + # @param [PauseIngestOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def pause_ingest(index_name, options = PauseIngestOptions.new) + @backend.search_index_pause_ingest(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Resumes updates and maintenance for an index + # + # @param [String] index_name name of the index + # @param [ResumeIngestOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def resume_ingest(index_name, options = ResumeIngestOptions.new) + @backend.search_index_resume_ingest(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Allows querying against the index + # + # @param [String] index_name name of the index + # @param [AllowQueryingOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def allow_querying(index_name, options = AllowQueryingOptions.new) + @backend.search_index_allow_querying(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Disallows querying against the index + # + # @param [String] index_name name of the index + # @param [DisallowQueryingOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def disallow_querying(index_name, options = DisallowQueryingOptions.new) + @backend.search_index_disallow_querying(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Freeze the assignment of index partitions to nodes + # + # @param [String] index_name name of the index + # @param [FreezePlanOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def freeze_plan(index_name, options = FreezePlanOptions.new) + @backend.search_index_freeze_plan(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Unfreeze the assignment of index partitions to nodes + # + # @param [String] index_name name of the index + # @param [UnfreezePlanOptions] options + # + # @return void + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def unfreeze_plan(index_name, options = UnfreezePlanOptions.new) + @backend.search_index_unfreeze_plan(@bucket_name, @scope_name, index_name, options.timeout) + end + + # Allows to see how a document is analyzed against a specific index + # + # @param [String] index_name name of the index + # @param [Hash] document the document to be analyzed + # + # @return [Array] + # + # @raise [ArgumentError] + # @raise [Error::IndexNotFound] + def analyze_document(index_name, document, options = AnalyzeDocumentOptions.new) + res = @backend.search_index_analyze_document(@bucket_name, @scope_name, index_name, JSON.generate(document), options.timeout) + JSON.parse(res[:analysis]) + end + + GetIndexOptions = SearchIndexManager::GetIndexOptions + GetAllIndexesOptions = SearchIndexManager::GetAllIndexesOptions + UpsertIndexOptions = SearchIndexManager::UpsertIndexOptions + DropIndexOptions = SearchIndexManager::DropIndexOptions + GetIndexedDocumentsCountOptions = SearchIndexManager::GetIndexedDocumentsCountOptions + PauseIngestOptions = SearchIndexManager::PauseIngestOptions + ResumeIngestOptions = SearchIndexManager::ResumeIngestOptions + AllowQueryingOptions = SearchIndexManager::AllowQueryingOptions + DisallowQueryingOptions = SearchIndexManager::DisallowQueryingOptions + FreezePlanOptions = SearchIndexManager::FreezePlanOptions + UnfreezePlanOptions = SearchIndexManager::UnfreezePlanOptions + AnalyzeDocumentOptions = SearchIndexManager::AnalyzeDocumentOptions + end + end +end diff --git a/lib/couchbase/management/search_index_manager.rb b/lib/couchbase/management/search_index_manager.rb index d813b6ed..e65d0227 100644 --- a/lib/couchbase/management/search_index_manager.rb +++ b/lib/couchbase/management/search_index_manager.rb @@ -34,8 +34,8 @@ def initialize(backend) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def get_index(index_name, options = GetIndexOptions.new) - res = @backend.search_index_get(index_name, options.timeout) - extract_search_index(res) + res = @backend.search_index_get(nil, nil, index_name, options.timeout) + self.class.extract_search_index(res) end # Fetches all indexes from the server @@ -44,8 +44,8 @@ def get_index(index_name, options = GetIndexOptions.new) # # @return [Array] def get_all_indexes(options = GetAllIndexesOptions.new) - res = @backend.search_index_get_all(options.timeout) - res[:indexes].map { |idx| extract_search_index(idx) } + res = @backend.search_index_get_all(nil, nil, options.timeout) + res[:indexes].map { |idx| self.class.extract_search_index(idx) } end # Creates or updates the index @@ -58,6 +58,8 @@ def get_all_indexes(options = GetAllIndexesOptions.new) # @raise [ArgumentError] if name, type or source_type is empty def upsert_index(index_definition, options = UpsertIndexOptions.new) @backend.search_index_upsert( + nil, + nil, { name: index_definition.name, type: index_definition.type, @@ -82,7 +84,7 @@ def upsert_index(index_definition, options = UpsertIndexOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def drop_index(index_name, options = DropIndexOptions.new) - @backend.search_index_drop(index_name, options.timeout) + @backend.search_index_drop(nil, nil, index_name, options.timeout) end # Retrieves the number of documents that have been indexed for an index @@ -95,7 +97,7 @@ def drop_index(index_name, options = DropIndexOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def get_indexed_documents_count(index_name, options = GetIndexedDocumentsCountOptions.new) - res = @backend.search_index_get_documents_count(index_name, options.timeout) + res = @backend.search_index_get_documents_count(nil, nil, index_name, options.timeout) res[:count] end @@ -140,7 +142,7 @@ def get_stats(options = GetIndexStatsOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def pause_ingest(index_name, options = PauseIngestOptions.new) - @backend.search_index_pause_ingest(index_name, options.timeout) + @backend.search_index_pause_ingest(nil, nil, index_name, options.timeout) end # Resumes updates and maintenance for an index @@ -153,7 +155,7 @@ def pause_ingest(index_name, options = PauseIngestOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def resume_ingest(index_name, options = ResumeIngestOptions.new) - @backend.search_index_resume_ingest(index_name, options.timeout) + @backend.search_index_resume_ingest(nil, nil, index_name, options.timeout) end # Allows querying against the index @@ -166,7 +168,7 @@ def resume_ingest(index_name, options = ResumeIngestOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def allow_querying(index_name, options = AllowQueryingOptions.new) - @backend.search_index_allow_querying(index_name, options.timeout) + @backend.search_index_allow_querying(nil, nil, index_name, options.timeout) end # Disallows querying against the index @@ -179,7 +181,7 @@ def allow_querying(index_name, options = AllowQueryingOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def disallow_querying(index_name, options = DisallowQueryingOptions.new) - @backend.search_index_disallow_querying(index_name, options.timeout) + @backend.search_index_disallow_querying(nil, nil, index_name, options.timeout) end # Freeze the assignment of index partitions to nodes @@ -192,7 +194,7 @@ def disallow_querying(index_name, options = DisallowQueryingOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def freeze_plan(index_name, options = FreezePlanOptions.new) - @backend.search_index_freeze_plan(index_name, options.timeout) + @backend.search_index_freeze_plan(nil, nil, index_name, options.timeout) end # Unfreeze the assignment of index partitions to nodes @@ -205,7 +207,7 @@ def freeze_plan(index_name, options = FreezePlanOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def unfreeze_plan(index_name, options = UnfreezePlanOptions.new) - @backend.search_index_unfreeze_plan(index_name, options.timeout) + @backend.search_index_unfreeze_plan(nil, nil, index_name, options.timeout) end # Allows to see how a document is analyzed against a specific index @@ -218,7 +220,7 @@ def unfreeze_plan(index_name, options = UnfreezePlanOptions.new) # @raise [ArgumentError] # @raise [Error::IndexNotFound] def analyze_document(index_name, document, options = AnalyzeDocumentOptions.new) - res = @backend.search_index_analyze_document(index_name, JSON.generate(document), options.timeout) + res = @backend.search_index_analyze_document(nil, nil, index_name, JSON.generate(document), options.timeout) JSON.parse(res[:analysis]) end @@ -352,9 +354,8 @@ def initialize end end - private - - def extract_search_index(resp) + # @api private + def self.extract_search_index(resp) SearchIndex.new do |index| index.name = resp[:name] index.type = resp[:type] @@ -403,6 +404,21 @@ def initialize @source_type = "couchbase" yield self if block_given? end + + # @api private + def to_backend + { + name: name, + type: type, + uuid: uuid, + params: (JSON.generate(params) if params), + source_name: source_name, + source_type: source_type, + source_uuid: source_uuid, + source_params: (JSON.generate(source_params) if source_params), + plan_params: (JSON.generate(plan_params) if plan_params), + } + end end end end diff --git a/lib/couchbase/scope.rb b/lib/couchbase/scope.rb index fb99ce3e..158e33de 100644 --- a/lib/couchbase/scope.rb +++ b/lib/couchbase/scope.rb @@ -143,8 +143,36 @@ def analytics_query(statement, options = Options::Analytics::DEFAULT) # # @return [SearchResult] def search_query(index_name, query, options = Options::Search::DEFAULT) - resp = @backend.document_search(index_name, JSON.generate(query), options.to_backend(scope_name: @name)) + resp = @backend.document_search(@bucket_name, @name, index_name, JSON.generate(query), {}, options.to_backend) + convert_search_result(resp, options) + end + + # Performs a request against the Full Text Search (FTS) service. + # + # @api volatile + # + # @param [String] index_name the name of the search index + # @param [SearchRequest] search_request the request + # @param [Options::Search] options the custom options for this search request + # + # @return [SearchResult] + def search(index_name, search_request, options = Options::Search::DEFAULT) + encoded_query, encoded_req = search_request.to_backend + resp = @backend.document_search(@bucket_name, @name, index_name, encoded_query, encoded_req, options.to_backend(show_request: false)) + convert_search_result(resp, options) + end + + # @api volatile + # + # @return [Management::ScopeSearchIndexManager] + def search_indexes + Management::ScopeSearchIndexManager.new(@backend, @bucket_name, @name) + end + + private + # @api private + def convert_search_result(resp, options) SearchResult.new do |res| res.meta_data = SearchMetaData.new do |meta| meta.metrics.max_score = resp[:meta_data][:metrics][:max_score] diff --git a/lib/couchbase/search_options.rb b/lib/couchbase/search_options.rb index a5f14e3f..ec65d687 100644 --- a/lib/couchbase/search_options.rb +++ b/lib/couchbase/search_options.rb @@ -13,1598 +13,1607 @@ # limitations under the License. module Couchbase - class Cluster - # @api volatile - class SearchRequest - # Creates a search request, used to perform operations against the Full Text Search (FTS) Couchbase service. - # - # @overload new(search_query) - # Will run an FTS +SearchQuery+ - # @param [SearchQuery] search_query - # - # @overload new(vector_search) - # Will run a +VectorSearch+ - # @param [VectorSearch] vector_search - def initialize(search) - case search - when SearchQuery - @search_query = search - when VectorSearch - @vector_search = search - else - raise Error::InvalidArgument, "Search type must be either SearchQuery or VectorSearch, #{search.class} given" - end + # @api volatile + class SearchRequest + # Creates a search request, used to perform operations against the Full Text Search (FTS) Couchbase service. + # + # @overload new(search_query) + # Will run an FTS +SearchQuery+ + # @param [SearchQuery] search_query + # + # @overload new(vector_search) + # Will run a +VectorSearch+ + # @param [VectorSearch] vector_search + def initialize(search) + case search + when SearchQuery + @search_query = search + when VectorSearch + @vector_search = search + else + raise Error::InvalidArgument, "Search type must be either SearchQuery or VectorSearch, #{search.class} given" end + end - # Can be used to run a +SearchQuery+ together with an existing +VectorSearch+ - # Note that a maximum of one +SearchQuery+ can be provided. - # - # @param [SearchQuery] query - # - # @return [SearchRequest] for chaining purposes - def search_query(query) - raise Error::InvalidArgument, "A SearchQuery has already been specified" unless @search_query.nil? + # Can be used to run a +SearchQuery+ together with an existing +VectorSearch+ + # Note that a maximum of one +SearchQuery+ can be provided. + # + # @param [SearchQuery] query + # + # @return [SearchRequest] for chaining purposes + def search_query(query) + raise Error::InvalidArgument, "A SearchQuery has already been specified" unless @search_query.nil? + + @search_query = query + self + end - @search_query = query - self - end + # Can be used to run a +VectorSearch+ together with an existing +SearchQuery+ + # Note that a maximum of one +VectorSearch+ can be provided. + # + # @param [VectorSearch] query + # + # @return [SearchRequest] for chaining purposes + def vector_search(query) + raise Error::InvalidArgument, "A VectorSearch has already been specified" unless @vector_search.nil? + + @vector_search = query + self + end - # Can be used to run a +VectorSearch+ together with an existing +SearchQuery+ - # Note that a maximum of one +VectorSearch+ can be provided. - # - # @param [VectorSearch] query - # - # @return [SearchRequest] for chaining purposes - def vector_search(query) - raise Error::InvalidArgument, "A VectorSearch has already been specified" unless @vector_search.nil? + # @api private + def to_backend + [ + (@search_query || SearchQuery.match_none).to_json, + { + vector_search: @vector_search&.to_backend, + }, + ] + end + end - @vector_search = query - self - end + class SearchQuery + # @return [Hash] + def to_h + {} + end - # @api private - def to_backend - [ - (@search_query || SearchQuery.match_none).to_json, - { - vector_search: @vector_search.to_backend, - }, - ] - end + # @return [String] + def to_json(*args) + to_h.to_json(*args) end - class SearchQuery - # @return [Hash] - def to_h - {} - end + # Prepare {MatchQuery} body + # + # @param [String] match + # @yieldparam [MatchQuery] query + # + # @return [MatchQuery] + def self.match(match, &block) + MatchQuery.new(match, &block) + end + + # A match query analyzes the input text and uses that analyzed text to query the index. + class MatchQuery < SearchQuery + # @return [Float] + attr_accessor :boost # @return [String] - def to_json(*args) - to_h.to_json(*args) - end + attr_accessor :field - # Prepare {MatchQuery} body - # - # @param [String] match - # @yieldparam [MatchQuery] query - # - # @return [MatchQuery] - def self.match(match, &block) - MatchQuery.new(match, &block) - end + # @return [String] + attr_accessor :analyzer - # A match query analyzes the input text and uses that analyzed text to query the index. - class MatchQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Integer] + attr_accessor :prefix_length - # @return [String] - attr_accessor :field + # @return [Integer] + attr_accessor :fuzziness - # @return [String] - attr_accessor :analyzer + # @return [nil, :or, :and] + attr_accessor :operator - # @return [Integer] - attr_accessor :prefix_length + # @param [String] match + # @yieldparam [MatchQuery] self + def initialize(match) + super() + @match = match + yield self if block_given? + end - # @return [Integer] - attr_accessor :fuzziness + # @return [Hash] + def to_h + data = {:match => @match} + data[:boost] = boost if boost + data[:field] = field if field + data[:analyzer] = analyzer if analyzer + data[:operator] = operator if operator + data[:fuzziness] = fuzziness if fuzziness + data[:prefix_length] = prefix_length if prefix_length + data + end + end - # @return [nil, :or, :and] - attr_accessor :operator + # Prepare {MatchPhraseQuery} body + # + # @param [String] match_phrase + # @yieldparam [MatchPhraseQuery] query + # + # @return [MatchPhraseQuery] + def self.match_phrase(match_phrase, &block) + MatchPhraseQuery.new(match_phrase, &block) + end - # @param [String] match - # @yieldparam [MatchQuery] self - def initialize(match) - super() - @match = match - yield self if block_given? - end + # The input text is analyzed and a phrase query is built with the terms resulting from the analysis. + class MatchPhraseQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [Hash] - def to_h - data = {:match => @match} - data[:boost] = boost if boost - data[:field] = field if field - data[:analyzer] = analyzer if analyzer - data[:operator] = operator if operator - data[:fuzziness] = fuzziness if fuzziness - data[:prefix_length] = prefix_length if prefix_length - data - end - end + # @return [String] + attr_accessor :field + + # @return [String] + attr_accessor :analyzer - # Prepare {MatchPhraseQuery} body - # # @param [String] match_phrase - # @yieldparam [MatchPhraseQuery] query # - # @return [MatchPhraseQuery] - def self.match_phrase(match_phrase, &block) - MatchPhraseQuery.new(match_phrase, &block) + # @yieldparam [MatchPhraseQuery] self + def initialize(match_phrase) + super() + @match_phrase = match_phrase + yield self if block_given? end - # The input text is analyzed and a phrase query is built with the terms resulting from the analysis. - class MatchPhraseQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field + # @return [Hash] + def to_h + data = {:match_phrase => @match_phrase} + data[:boost] = boost if boost + data[:field] = field if field + data[:analyzer] = analyzer if analyzer + data + end + end - # @return [String] - attr_accessor :analyzer + # Prepare {RegexpQuery} body + # + # @param [String] regexp + # @yieldparam [RegexpQuery] query + # + # @return [RegexpQuery] + def self.regexp(regexp, &block) + RegexpQuery.new(regexp, &block) + end - # @param [String] match_phrase - # - # @yieldparam [MatchPhraseQuery] self - def initialize(match_phrase) - super() - @match_phrase = match_phrase - yield self if block_given? - end + # Finds documents containing terms that match the specified regular expression. + class RegexpQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [Hash] - def to_h - data = {:match_phrase => @match_phrase} - data[:boost] = boost if boost - data[:field] = field if field - data[:analyzer] = analyzer if analyzer - data - end - end + # @return [String] + attr_accessor :field - # Prepare {RegexpQuery} body - # # @param [String] regexp - # @yieldparam [RegexpQuery] query # - # @return [RegexpQuery] - def self.regexp(regexp, &block) - RegexpQuery.new(regexp, &block) + # @yieldparam [RegexpQuery] self + def initialize(regexp) + super() + @regexp = regexp + yield self if block_given? end - # Finds documents containing terms that match the specified regular expression. - class RegexpQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field + # @return [Hash] + def to_h + data = {:regexp => @regexp} + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @param [String] regexp - # - # @yieldparam [RegexpQuery] self - def initialize(regexp) - super() - @regexp = regexp - yield self if block_given? - end + # Prepare {QueryStringQuery} body + # + # @param [String] query_string + # @yieldparam [QueryStringQuery] query + # + # @return [QueryStringQuery] + def self.query_string(query_string, &block) + QueryStringQuery.new(query_string, &block) + end - # @return [Hash] - def to_h - data = {:regexp => @regexp} - data[:boost] = boost if boost - data[:field] = field if field - data - end - end + # The query string query allows humans to describe complex queries using a simple syntax. + class QueryStringQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # Prepare {QueryStringQuery} body - # # @param [String] query_string - # @yieldparam [QueryStringQuery] query # - # @return [QueryStringQuery] - def self.query_string(query_string, &block) - QueryStringQuery.new(query_string, &block) + # @yieldparam [QueryStringQuery] self + def initialize(query_string) + super() + @query_string = query_string + yield self if block_given? end - # The query string query allows humans to describe complex queries using a simple syntax. - class QueryStringQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Hash] + def to_h + data = {:query => @query_string} + data[:boost] = boost if boost + data + end + end - # @param [String] query_string - # - # @yieldparam [QueryStringQuery] self - def initialize(query_string) - super() - @query_string = query_string - yield self if block_given? - end + # Prepare {WildcardQuery} body + # + # @param [String] wildcard + # @yieldparam [WildcardQuery] query + # + # @return [WildcardQuery] + def self.wildcard(wildcard, &block) + WildcardQuery.new(wildcard, &block) + end - # @return [Hash] - def to_h - data = {:query => @query_string} - data[:boost] = boost if boost - data - end - end + # Interprets * and ? wildcards as found in a lot of applications, for an easy implementation of such a search feature. + class WildcardQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field - # Prepare {WildcardQuery} body - # # @param [String] wildcard - # @yieldparam [WildcardQuery] query # - # @return [WildcardQuery] - def self.wildcard(wildcard, &block) - WildcardQuery.new(wildcard, &block) + # @yieldparam [WildcardQuery] self + def initialize(wildcard) + super() + @wildcard = wildcard + yield self if block_given? end - # Interprets * and ? wildcards as found in a lot of applications, for an easy implementation of such a search feature. - class WildcardQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field + # @return [Hash] + def to_h + data = {:wildcard => @wildcard} + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @param [String] wildcard - # - # @yieldparam [WildcardQuery] self - def initialize(wildcard) - super() - @wildcard = wildcard - yield self if block_given? - end + # Prepare {DocIdQuery} body + # + # @param [String...] doc_ids + # @yieldparam [DocIdQuery] query + # + # @return [DocIdQuery] + def self.doc_id(*doc_ids) + DocIdQuery.new(*doc_ids) + end - # @return [Hash] - def to_h - data = {:wildcard => @wildcard} - data[:boost] = boost if boost - data[:field] = field if field - data - end - end + # Allows to restrict matches to a set of specific documents. + class DocIdQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # Prepare {DocIdQuery} body - # # @param [String...] doc_ids - # @yieldparam [DocIdQuery] query # - # @return [DocIdQuery] - def self.doc_id(*doc_ids) - DocIdQuery.new(*doc_ids) + # @yieldparam [DocIdQuery] self + def initialize(*doc_ids) + super() + @doc_ids = doc_ids + yield self if block_given? end - # Allows to restrict matches to a set of specific documents. - class DocIdQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Hash] + def to_h + data = {:ids => @doc_ids.flatten.uniq} + data[:boost] = boost if boost + data + end + end - # @param [String...] doc_ids - # - # @yieldparam [DocIdQuery] self - def initialize(*doc_ids) - super() - @doc_ids = doc_ids - yield self if block_given? - end + # Prepare {BooleanFieldQuery} body + # + # @param [Boolean] value + # @yieldparam [BooleanFieldQuery] query + # + # @return [BooleanFieldQuery] + def self.boolean_field(value) + BooleanFieldQuery.new(value) + end - # @return [Hash] - def to_h - data = {:ids => @doc_ids.flatten.uniq} - data[:boost] = boost if boost - data - end - end + # Allow to match `true`/`false` in a field mapped as boolean. + class BooleanFieldQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field - # Prepare {BooleanFieldQuery} body - # # @param [Boolean] value - # @yieldparam [BooleanFieldQuery] query # - # @return [BooleanFieldQuery] - def self.boolean_field(value) - BooleanFieldQuery.new(value) + # @yieldparam [BooleanFieldQuery] self + def initialize(value) + super() + @value = value + yield self if block_given? end - # Allow to match `true`/`false` in a field mapped as boolean. - class BooleanFieldQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Hash] + def to_h + data = {:bool => @value} + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @return [String] - attr_accessor :field + # Prepare {DateRangeQuery} body + # + # @yieldparam [DateRangeQuery] query + # + # @return [DateRangeQuery] + def self.date_range(&block) + DateRangeQuery.new(&block) + end - # @param [Boolean] value - # - # @yieldparam [BooleanFieldQuery] self - def initialize(value) - super() - @value = value - yield self if block_given? - end + # The date range query finds documents containing a date value in the specified field within the specified range. + class DateRangeQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [Hash] - def to_h - data = {:bool => @value} - data[:boost] = boost if boost - data[:field] = field if field - data - end - end + # @return [String] + attr_accessor :field + + # @return [String] + attr_accessor :date_time_parser - # Prepare {DateRangeQuery} body + # Sets the lower boundary of the range. # - # @yieldparam [DateRangeQuery] query + # @note The lower boundary is considered inclusive by default on the server side. # - # @return [DateRangeQuery] - def self.date_range(&block) - DateRangeQuery.new(&block) + # @param [Time, String] time_point start time. When +Time+ object is passed {#date_time_parser} must be +nil+ (to use server + # default) + # @param [Boolean] inclusive + def start_time(time_point, inclusive = nil) + @start_time = time_point + @start_inclusive = inclusive end - # The date range query finds documents containing a date value in the specified field within the specified range. - class DateRangeQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field - - # @return [String] - attr_accessor :date_time_parser - - # Sets the lower boundary of the range. - # - # @note The lower boundary is considered inclusive by default on the server side. - # - # @param [Time, String] time_point start time. When +Time+ object is passed {#date_time_parser} must be +nil+ (to use server - # default) - # @param [Boolean] inclusive - def start_time(time_point, inclusive = nil) - @start_time = time_point - @start_inclusive = inclusive - end + # Sets the upper boundary of the range. + # + # @note The upper boundary is considered exclusive by default on the server side. + # + # @param [Time, String] time_point end time. When +Time+ object is passed {#date_time_parser} must be +nil+ (to use server default) + # @param [Boolean] inclusive + def end_time(time_point, inclusive = nil) + @end_time = time_point + @end_inclusive = inclusive + end - # Sets the upper boundary of the range. - # - # @note The upper boundary is considered exclusive by default on the server side. - # - # @param [Time, String] time_point end time. When +Time+ object is passed {#date_time_parser} must be +nil+ (to use server default) - # @param [Boolean] inclusive - def end_time(time_point, inclusive = nil) - @end_time = time_point - @end_inclusive = inclusive - end + # @yieldparam [DateRangeQuery] self + def initialize + super + @start_time = nil + @start_inclusive = nil + @end_time = nil + @end_inclusive = nil + yield self if block_given? + end - # @yieldparam [DateRangeQuery] self - def initialize - super - @start_time = nil - @start_inclusive = nil - @end_time = nil - @end_inclusive = nil - yield self if block_given? - end + DATE_FORMAT_RFC3339 = "%Y-%m-%dT%H:%M:%S%:z".freeze - DATE_FORMAT_RFC3339 = "%Y-%m-%dT%H:%M:%S%:z".freeze - - # @return [Hash] - def to_h - raise ArgumentError, "either start_time or end_time must be set for DateRangeQuery" if @start_time.nil? && @end_time.nil? - - data = {} - data[:boost] = boost if boost - data[:field] = field if field - data[:datetime_parser] = date_time_parser if date_time_parser - if @start_time - data[:start] = if @start_time.respond_to?(:strftime) - @start_time.strftime(DATE_FORMAT_RFC3339) - else - @start_time - end - data[:inclusive_start] = @start_inclusive unless @start_inclusive.nil? - end - if @end_time - data[:end] = if @end_time.respond_to?(:strftime) - @end_time.strftime(DATE_FORMAT_RFC3339) + # @return [Hash] + def to_h + raise ArgumentError, "either start_time or end_time must be set for DateRangeQuery" if @start_time.nil? && @end_time.nil? + + data = {} + data[:boost] = boost if boost + data[:field] = field if field + data[:datetime_parser] = date_time_parser if date_time_parser + if @start_time + data[:start] = if @start_time.respond_to?(:strftime) + @start_time.strftime(DATE_FORMAT_RFC3339) else - @end_time + @start_time end - data[:inclusive_end] = @end_inclusive unless @end_inclusive.nil? - end - data + data[:inclusive_start] = @start_inclusive unless @start_inclusive.nil? end + if @end_time + data[:end] = if @end_time.respond_to?(:strftime) + @end_time.strftime(DATE_FORMAT_RFC3339) + else + @end_time + end + data[:inclusive_end] = @end_inclusive unless @end_inclusive.nil? + end + data end + end + + # Prepare {NumericRangeQuery} body + # + # @yieldparam [NumericRangeQuery] query + # + # @return [NumericRangeQuery] + def self.numeric_range(&block) + NumericRangeQuery.new(&block) + end - # Prepare {NumericRangeQuery} body + # The numeric range query finds documents containing a numeric value in the specified field within the specified range. + class NumericRangeQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field + + # Sets lower bound of the range. # - # @yieldparam [NumericRangeQuery] query + # The lower boundary is considered inclusive by default on the server side. # - # @return [NumericRangeQuery] - def self.numeric_range(&block) - NumericRangeQuery.new(&block) + # @param [Numeric] lower_bound + # @param [Boolean] inclusive + def min(lower_bound, inclusive = nil) + @min = lower_bound + @min_inclusive = inclusive end - # The numeric range query finds documents containing a numeric value in the specified field within the specified range. - class NumericRangeQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # Sets upper bound of the range. + # + # The upper boundary is considered exclusive by default on the server side. + # + # @param [Numeric] upper_bound + # @param [Boolean] inclusive + def max(upper_bound, inclusive = nil) + @max = upper_bound + @max_inclusive = inclusive + end - # @return [String] - attr_accessor :field - - # Sets lower bound of the range. - # - # The lower boundary is considered inclusive by default on the server side. - # - # @param [Numeric] lower_bound - # @param [Boolean] inclusive - def min(lower_bound, inclusive = nil) - @min = lower_bound - @min_inclusive = inclusive - end + # @yieldparam [NumericRangeQuery] self + def initialize + super + @min = nil + @min_inclusive = nil + @max = nil + @max_inclusive = nil + yield self if block_given? + end - # Sets upper bound of the range. - # - # The upper boundary is considered exclusive by default on the server side. - # - # @param [Numeric] upper_bound - # @param [Boolean] inclusive - def max(upper_bound, inclusive = nil) - @max = upper_bound - @max_inclusive = inclusive - end + # @return [Hash] + def to_h + raise ArgumentError, "either min or max must be set for NumericRangeQuery" if @min.nil? && @max.nil? - # @yieldparam [NumericRangeQuery] self - def initialize - super - @min = nil - @min_inclusive = nil - @max = nil - @max_inclusive = nil - yield self if block_given? + data = {} + data[:boost] = boost if boost + data[:field] = field if field + if @min + data[:min] = @min + data[:inclusive_min] = @min_inclusive unless @min_inclusive.nil? end - - # @return [Hash] - def to_h - raise ArgumentError, "either min or max must be set for NumericRangeQuery" if @min.nil? && @max.nil? - - data = {} - data[:boost] = boost if boost - data[:field] = field if field - if @min - data[:min] = @min - data[:inclusive_min] = @min_inclusive unless @min_inclusive.nil? - end - if @max - data[:max] = @max - data[:inclusive_max] = @max_inclusive unless @max_inclusive.nil? - end - data + if @max + data[:max] = @max + data[:inclusive_max] = @max_inclusive unless @max_inclusive.nil? end + data end + end + + # Prepare {TermRangeQuery} body + # + # @yieldparam [TermRangeQuery] query + # + # @return [TermRangeQuery] + def self.term_range(&block) + TermRangeQuery.new(&block) + end + + # The term range query finds documents containing a string value in the specified field within the specified range. + class TermRangeQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field - # Prepare {TermRangeQuery} body + # Sets lower bound of the range. # - # @yieldparam [TermRangeQuery] query + # The lower boundary is considered inclusive by default on the server side. # - # @return [TermRangeQuery] - def self.term_range(&block) - TermRangeQuery.new(&block) + # @param [String] lower_bound + # @param [Boolean] inclusive + def min(lower_bound, inclusive = nil) + @min = lower_bound + @min_inclusive = inclusive end - # The term range query finds documents containing a string value in the specified field within the specified range. - class TermRangeQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # Sets upper bound of the range. + # + # The upper boundary is considered exclusive by default on the server side. + # + # @param [String] upper_bound + # @param [Boolean] inclusive + def max(upper_bound, inclusive = nil) + @max = upper_bound + @max_inclusive = inclusive + end - # @return [String] - attr_accessor :field - - # Sets lower bound of the range. - # - # The lower boundary is considered inclusive by default on the server side. - # - # @param [String] lower_bound - # @param [Boolean] inclusive - def min(lower_bound, inclusive = nil) - @min = lower_bound - @min_inclusive = inclusive - end + # @yieldparam [TermRangeQuery] self + def initialize + super + @min = nil + @min_inclusive = nil + @max = nil + @max_inclusive = nil + yield self if block_given? + end - # Sets upper bound of the range. - # - # The upper boundary is considered exclusive by default on the server side. - # - # @param [String] upper_bound - # @param [Boolean] inclusive - def max(upper_bound, inclusive = nil) - @max = upper_bound - @max_inclusive = inclusive - end + # @return [Hash] + def to_h + raise ArgumentError, "either min or max must be set for TermRangeQuery" if @min.nil? && @max.nil? - # @yieldparam [TermRangeQuery] self - def initialize - super - @min = nil - @min_inclusive = nil - @max = nil - @max_inclusive = nil - yield self if block_given? + data = {} + data[:boost] = boost if boost + data[:field] = field if field + if @min + data[:min] = @min + data[:inclusive_min] = @min_inclusive unless @min_inclusive.nil? end - - # @return [Hash] - def to_h - raise ArgumentError, "either min or max must be set for TermRangeQuery" if @min.nil? && @max.nil? - - data = {} - data[:boost] = boost if boost - data[:field] = field if field - if @min - data[:min] = @min - data[:inclusive_min] = @min_inclusive unless @min_inclusive.nil? - end - if @max - data[:max] = @max - data[:inclusive_max] = @max_inclusive unless @max_inclusive.nil? - end - data + if @max + data[:max] = @max + data[:inclusive_max] = @max_inclusive unless @max_inclusive.nil? end + data end + end - # Prepare {GeoDistanceQuery} body - # - # @yieldparam [GeoDistanceQuery] query - # - # @param [Float] latitude location latitude - # @param [Float] longitude location longitude - # @param [String] distance how big is area (number with units) - # - # @return [GeoDistanceQuery] - def self.geo_distance(longitude, latitude, distance, &block) - GeoDistanceQuery.new(longitude, latitude, distance, &block) - end + # Prepare {GeoDistanceQuery} body + # + # @yieldparam [GeoDistanceQuery] query + # + # @param [Float] latitude location latitude + # @param [Float] longitude location longitude + # @param [String] distance how big is area (number with units) + # + # @return [GeoDistanceQuery] + def self.geo_distance(longitude, latitude, distance, &block) + GeoDistanceQuery.new(longitude, latitude, distance, &block) + end - # Finds `geopoint` indexed matches around a point with the given distance. - class GeoDistanceQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # Finds `geopoint` indexed matches around a point with the given distance. + class GeoDistanceQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [String] - attr_accessor :field + # @return [String] + attr_accessor :field - # @yieldparam [GeoDistanceQuery] self - # @param [Float] longitude - # @param [Float] latitude - # @param [Float] distance - def initialize(longitude, latitude, distance) - super() - @longitude = longitude - @latitude = latitude - @distance = distance - yield self if block_given? - end + # @yieldparam [GeoDistanceQuery] self + # @param [Float] longitude + # @param [Float] latitude + # @param [Float] distance + def initialize(longitude, latitude, distance) + super() + @longitude = longitude + @latitude = latitude + @distance = distance + yield self if block_given? + end - # @return [Hash] - def to_h - data = { - :location => [@longitude, @latitude], - :distance => @distance, - } - data[:boost] = boost if boost - data[:field] = field if field - data - end + # @return [Hash] + def to_h + data = { + :location => [@longitude, @latitude], + :distance => @distance, + } + data[:boost] = boost if boost + data[:field] = field if field + data end + end - # Prepare {GeoBoundingBoxQuery} body - # - # @yieldparam [GeoBoundingBoxQuery] query + # Prepare {GeoBoundingBoxQuery} body + # + # @yieldparam [GeoBoundingBoxQuery] query + # + # @param [Float] top_left_longitude + # @param [Float] top_left_latitude + # @param [Float] bottom_right_longitude + # @param [Float] bottom_right_latitude + # + # @return [GeoBoundingBoxQuery] + def self.geo_bounding_box(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude, &block) + GeoBoundingBoxQuery.new(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude, &block) + end + + # Finds `geopoint` indexed matches in a given bounding box. + class GeoBoundingBoxQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field + + # @yieldparam [GeoBoundingBoxQuery] self # # @param [Float] top_left_longitude # @param [Float] top_left_latitude # @param [Float] bottom_right_longitude # @param [Float] bottom_right_latitude - # - # @return [GeoBoundingBoxQuery] - def self.geo_bounding_box(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude, &block) - GeoBoundingBoxQuery.new(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude, &block) + def initialize(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude) + super() + @top_left_longitude = top_left_longitude + @top_left_latitude = top_left_latitude + @bottom_right_longitude = bottom_right_longitude + @bottom_right_latitude = bottom_right_latitude + yield self if block_given? end - # Finds `geopoint` indexed matches in a given bounding box. - class GeoBoundingBoxQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Hash] + def to_h + data = { + :top_left => [@top_left_longitude, @top_left_latitude], + :bottom_right => [@bottom_right_longitude, @bottom_right_latitude], + } + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @return [String] - attr_accessor :field - - # @yieldparam [GeoBoundingBoxQuery] self - # - # @param [Float] top_left_longitude - # @param [Float] top_left_latitude - # @param [Float] bottom_right_longitude - # @param [Float] bottom_right_latitude - def initialize(top_left_longitude, top_left_latitude, bottom_right_longitude, bottom_right_latitude) - super() - @top_left_longitude = top_left_longitude - @top_left_latitude = top_left_latitude - @bottom_right_longitude = bottom_right_longitude - @bottom_right_latitude = bottom_right_latitude - yield self if block_given? - end + # A coordinate is a tuple of a latitude and a longitude. + # + # @see GeoPolygonQuery + # @see SearchQuery.geo_polygon + class Coordinate + # @return [Float] + attr_accessor :longitude - # @return [Hash] - def to_h - data = { - :top_left => [@top_left_longitude, @top_left_latitude], - :bottom_right => [@bottom_right_longitude, @bottom_right_latitude], - } - data[:boost] = boost if boost - data[:field] = field if field - data - end - end + # @return [Float] + attr_accessor :latitude - # A coordinate is a tuple of a latitude and a longitude. - # - # @see GeoPolygonQuery - # @see SearchQuery.geo_polygon - class Coordinate - # @return [Float] - attr_accessor :longitude - - # @return [Float] - attr_accessor :latitude - - def initialize(longitude, latitude) - @longitude = longitude - @latitude = latitude - end + def initialize(longitude, latitude) + @longitude = longitude + @latitude = latitude end + end - # Prepare {GeoPolygonQuery} body - # - # @yieldparam [GeoPolygonQuery] query + # Prepare {GeoPolygonQuery} body + # + # @yieldparam [GeoPolygonQuery] query + # + # @param [Array] coordinates list of coordinates that forms polygon + # + # @return [GeoPolygonQuery] + # + # @api uncommitted + def self.geo_polygon(coordinates, &block) + GeoPolygonQuery.new(coordinates, &block) + end + + # A search query which allows to match inside a geo polygon. + class GeoPolygonQuery < SearchQuery + # @return [Float] + attr_accessor :boost + + # @return [String] + attr_accessor :field + + # @yieldparam [GeoPolygonQuery] self # # @param [Array] coordinates list of coordinates that forms polygon - # - # @return [GeoPolygonQuery] - # - # @api uncommitted - def self.geo_polygon(coordinates, &block) - GeoPolygonQuery.new(coordinates, &block) + def initialize(coordinates) + super() + @coordinates = coordinates + yield self if block_given? end - # A search query which allows to match inside a geo polygon. - class GeoPolygonQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @return [Hash] + def to_h + data = { + :polygon_points => @coordinates.map { |coord| [coord.longitude, coord.latitude] }, + } + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @return [String] - attr_accessor :field + # Prepare {ConjunctionQuery} body + # + # @yieldparam [ConjunctionQuery] query + # + # @return [ConjunctionQuery] + def self.conjuncts(...) + ConjunctionQuery.new(...) + end - # @yieldparam [GeoPolygonQuery] self - # - # @param [Array] coordinates list of coordinates that forms polygon - def initialize(coordinates) - super() - @coordinates = coordinates - yield self if block_given? - end + # Result documents must satisfy all of the child queries. + class ConjunctionQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [Hash] - def to_h - data = { - :polygon_points => @coordinates.map { |coord| [coord.longitude, coord.latitude] }, - } - data[:boost] = boost if boost - data[:field] = field if field - data - end + # @yieldparam [ConjunctionQuery] self + # + # @param [*SearchQuery] queries + def initialize(*queries) + super() + @queries = queries.flatten + yield self if block_given? end - # Prepare {ConjunctionQuery} body - # - # @yieldparam [ConjunctionQuery] query - # - # @return [ConjunctionQuery] - def self.conjuncts(...) - ConjunctionQuery.new(...) + # @param [*SearchQuery] queries + def and_also(*queries) + @queries |= queries.flatten end - # Result documents must satisfy all of the child queries. - class ConjunctionQuery < SearchQuery - # @return [Float] - attr_accessor :boost + def empty? + @queries.empty? + end - # @yieldparam [ConjunctionQuery] self - # - # @param [*SearchQuery] queries - def initialize(*queries) - super() - @queries = queries.flatten - yield self if block_given? - end + # @return [Hash] + def to_h + raise ArgumentError, "compound conjunction query must have sub-queries" if @queries.nil? || @queries.empty? - # @param [*SearchQuery] queries - def and_also(*queries) - @queries |= queries.flatten - end + data = {:conjuncts => @queries.uniq.map(&:to_h)} + data[:boost] = boost if boost + data + end + end - def empty? - @queries.empty? - end + # Prepare {ConjunctionQuery} body + # + # @yieldparam [DisjunctionQuery] query + # + # @return [DisjunctionQuery] + def self.disjuncts(...) + DisjunctionQuery.new(...) + end - # @return [Hash] - def to_h - raise ArgumentError, "compound conjunction query must have sub-queries" if @queries.nil? || @queries.empty? + # Result documents must satisfy a configurable min number of child queries. + class DisjunctionQuery < SearchQuery + # @return [Float] + attr_accessor :boost - data = {:conjuncts => @queries.uniq.map(&:to_h)} - data[:boost] = boost if boost - data - end - end + # @return [Integer] + attr_accessor :min - # Prepare {ConjunctionQuery} body - # - # @yieldparam [DisjunctionQuery] query + # @yieldparam [DisjunctionQuery] self # - # @return [DisjunctionQuery] - def self.disjuncts(...) - DisjunctionQuery.new(...) + # @param [*SearchQuery] queries + def initialize(*queries) + super() + @queries = queries.flatten + yield self if block_given? end - # Result documents must satisfy a configurable min number of child queries. - class DisjunctionQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @param [*SearchQuery] queries + def or_else(*queries) + @queries |= queries.flatten + end - # @return [Integer] - attr_accessor :min + def empty? + @queries.empty? + end - # @yieldparam [DisjunctionQuery] self - # - # @param [*SearchQuery] queries - def initialize(*queries) - super() - @queries = queries.flatten - yield self if block_given? - end + # @return [Hash] + def to_h + raise ArgumentError, "compound disjunction query must have sub-queries" if @queries.nil? || @queries.empty? - # @param [*SearchQuery] queries - def or_else(*queries) - @queries |= queries.flatten - end + data = {:disjuncts => @queries.uniq.map(&:to_h)} + if min + raise ArgumentError, "disjunction query has fewer sub-queries than configured minimum" if @queries.size < min - def empty? - @queries.empty? + data[:min] = min end + data[:boost] = boost if boost + data + end + end - # @return [Hash] - def to_h - raise ArgumentError, "compound disjunction query must have sub-queries" if @queries.nil? || @queries.empty? + # Prepare {BooleanQuery} body + # + # @yieldparam [BooleanQuery] query + # + # @return [BooleanQuery] + def self.booleans(&block) + BooleanQuery.new(&block) + end - data = {:disjuncts => @queries.uniq.map(&:to_h)} - if min - raise ArgumentError, "disjunction query has fewer sub-queries than configured minimum" if @queries.size < min + # The boolean query is a useful combination of conjunction and disjunction queries. + class BooleanQuery < SearchQuery + # @return [Float] + attr_accessor :boost - data[:min] = min - end - data[:boost] = boost if boost - data - end + # @yieldparam [BooleanQuery] self + def initialize + super() + @must = ConjunctionQuery.new + @must_not = DisjunctionQuery.new + @should = DisjunctionQuery.new + yield self if block_given? end - # Prepare {BooleanQuery} body - # - # @yieldparam [BooleanQuery] query - # - # @return [BooleanQuery] - def self.booleans(&block) - BooleanQuery.new(&block) + # @param [Integer] min minimal value for "should" disjunction query + def should_min(min) + @should.min = min + self end - # The boolean query is a useful combination of conjunction and disjunction queries. - class BooleanQuery < SearchQuery - # @return [Float] - attr_accessor :boost + # @param [*SearchQuery] queries + def must(*queries) + @must.and_also(*queries) + self + end - # @yieldparam [BooleanQuery] self - def initialize - super() - @must = ConjunctionQuery.new - @must_not = DisjunctionQuery.new - @should = DisjunctionQuery.new - yield self if block_given? - end + # @param [*SearchQuery] queries + def must_not(*queries) + @must_not.or_else(*queries) + self + end - # @param [Integer] min minimal value for "should" disjunction query - def should_min(min) - @should.min = min - self - end + # @param [*SearchQuery] queries + def should(*queries) + @should.or_else(*queries) + self + end - # @param [*SearchQuery] queries - def must(*queries) - @must.and_also(*queries) - self - end + # @return [Hash] + def to_h + raise ArgumentError, "BooleanQuery must have at least one non-empty sub-query" if @must.empty? && @must_not.empty? && @should.empty? - # @param [*SearchQuery] queries - def must_not(*queries) - @must_not.or_else(*queries) - self - end + data = {} + data[:must] = @must.to_h unless @must.empty? + data[:must_not] = @must_not.to_h unless @must_not.empty? + data[:should] = @should.to_h unless @should.empty? + data[:boost] = boost if boost + data + end + end - # @param [*SearchQuery] queries - def should(*queries) - @should.or_else(*queries) - self - end + # Prepare {TermQuery} body + # + # @yieldparam [TermQuery] query + # @param [String] term + # + # @return [TermQuery] + def self.term(term, &block) + TermQuery.new(term, &block) + end - # @return [Hash] - def to_h - if @must.empty? && @must_not.empty? && @should.empty? - raise ArgumentError, "BooleanQuery must have at least one non-empty sub-query" - end - - data = {} - data[:must] = @must.to_h unless @must.empty? - data[:must_not] = @must_not.to_h unless @must_not.empty? - data[:should] = @should.to_h unless @should.empty? - data[:boost] = boost if boost - data - end - end + # A query that looks for **exact** matches of the term in the index (no analyzer, no stemming). Useful to check what the actual + # content of the index is. It can also apply fuzziness on the term. Usual better alternative is `MatchQuery`. + class TermQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # Prepare {TermQuery} body + # @return [String] + attr_accessor :field + + # @return [Integer] + attr_accessor :fuzziness + + # @return [Integer] + attr_accessor :prefix_length + + # @yieldparam [TermQuery] self # - # @yieldparam [TermQuery] query # @param [String] term - # - # @return [TermQuery] - def self.term(term, &block) - TermQuery.new(term, &block) + def initialize(term) + super() + @term = term + yield self if block_given? end - # A query that looks for **exact** matches of the term in the index (no analyzer, no stemming). Useful to check what the actual - # content of the index is. It can also apply fuzziness on the term. Usual better alternative is `MatchQuery`. - class TermQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field + # @return [Hash] + def to_h + data = {:term => @term} + data[:boost] = boost if boost + data[:field] = field if field + if fuzziness + data[:fuzziness] = fuzziness + data[:prefix_length] = prefix_length if prefix_length + end + data + end + end - # @return [Integer] - attr_accessor :fuzziness + # Prepare {PrefixQuery} body + # + # @yieldparam [PrefixQuery] query + # @param [String] prefix + # + # @return [PrefixQuery] + def self.prefix(prefix, &block) + PrefixQuery.new(prefix, &block) + end - # @return [Integer] - attr_accessor :prefix_length + # The prefix query finds documents containing terms that start with the provided prefix. Usual better alternative is `MatchQuery`. + class PrefixQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @yieldparam [TermQuery] self - # - # @param [String] term - def initialize(term) - super() - @term = term - yield self if block_given? - end + # @return [nil, :or, :and] + attr_accessor :operator - # @return [Hash] - def to_h - data = {:term => @term} - data[:boost] = boost if boost - data[:field] = field if field - if fuzziness - data[:fuzziness] = fuzziness - data[:prefix_length] = prefix_length if prefix_length - end - data - end - end + # @return [String] + attr_accessor :field - # Prepare {PrefixQuery} body + # @yieldparam [PrefixQuery] self # - # @yieldparam [PrefixQuery] query # @param [String] prefix - # - # @return [PrefixQuery] - def self.prefix(prefix, &block) - PrefixQuery.new(prefix, &block) + def initialize(prefix) + super() + @prefix = prefix + yield self if block_given? end - # The prefix query finds documents containing terms that start with the provided prefix. Usual better alternative is `MatchQuery`. - class PrefixQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [nil, :or, :and] - attr_accessor :operator + # @return [Hash] + def to_h + data = {:prefix => @prefix} + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @return [String] - attr_accessor :field + # Prepare {PhraseQuery} body + # + # Creates a new instances {PhraseQuery} passing all parameters into {PhraseQuery#initialize}. + # + # @return [PhraseQuery] + def self.phrase(...) + PhraseQuery.new(...) + end - # @yieldparam [PrefixQuery] self - # - # @param [String] prefix - def initialize(prefix) - super() - @prefix = prefix - yield self if block_given? - end + # A query that looks for **exact** match of several terms (in the exact order) in the index. Usual better alternative is + # {MatchPhraseQuery}. + class PhraseQuery < SearchQuery + # @return [Float] + attr_accessor :boost - # @return [Hash] - def to_h - data = {:prefix => @prefix} - data[:boost] = boost if boost - data[:field] = field if field - data - end - end + # @return [String] + attr_accessor :field - # Prepare {PhraseQuery} body + # @yieldparam [PhraseQuery] self # - # Creates a new instances {PhraseQuery} passing all parameters into {PhraseQuery#initialize}. - # - # @return [PhraseQuery] - def self.phrase(...) - PhraseQuery.new(...) + # @param [*String] terms + def initialize(*terms) + super() + @terms = terms.flatten + yield self if block_given? end - # A query that looks for **exact** match of several terms (in the exact order) in the index. Usual better alternative is - # {MatchPhraseQuery}. - class PhraseQuery < SearchQuery - # @return [Float] - attr_accessor :boost - - # @return [String] - attr_accessor :field + # @return [Hash] + def to_h + data = {:terms => @terms.flatten.uniq} + data[:boost] = boost if boost + data[:field] = field if field + data + end + end - # @yieldparam [PhraseQuery] self - # - # @param [*String] terms - def initialize(*terms) - super() - @terms = terms.flatten - yield self if block_given? - end + # Prepare {MatchAllQuery} body + # + # @yieldparam [MatchAllQuery] query + # + # @return [MatchAllQuery] + def self.match_all(&block) + MatchAllQuery.new(&block) + end - # @return [Hash] - def to_h - data = {:terms => @terms.flatten.uniq} - data[:boost] = boost if boost - data[:field] = field if field - data - end + # A query that matches all indexed documents. + class MatchAllQuery < SearchQuery + # @yieldparam [MatchAllQuery] self + def initialize + super() + yield self if block_given? end - # Prepare {MatchAllQuery} body - # - # @yieldparam [MatchAllQuery] query - # - # @return [MatchAllQuery] - def self.match_all(&block) - MatchAllQuery.new(&block) + # @return [Hash] + def to_h + {:match_all => nil} end + end - # A query that matches all indexed documents. - class MatchAllQuery < SearchQuery - # @yieldparam [MatchAllQuery] self - def initialize - super() - yield self if block_given? - end + # Prepare {MatchNoneQuery} body + # + # @yieldparam [MatchNoneQuery] query + # + # @return [MatchNoneQuery] + def self.match_none(&block) + MatchNoneQuery.new(&block) + end - # @return [Hash] - def to_h - {:match_all => nil} - end + # A query that matches nothing. + class MatchNoneQuery < SearchQuery + # @yieldparam [MatchNoneQuery] self + def initialize + super() + yield self if block_given? end - # Prepare {MatchNoneQuery} body - # - # @yieldparam [MatchNoneQuery] query - # - # @return [MatchNoneQuery] - def self.match_none(&block) - MatchNoneQuery.new(&block) + # @return [Hash] + def to_h + {:match_none => nil} end + end + end - # A query that matches nothing. - class MatchNoneQuery < SearchQuery - # @yieldparam [MatchNoneQuery] self - def initialize - super() - yield self if block_given? - end + # @api volatile + class VectorSearch + # Constructs a +VectorSearch+ instance, which allows one or more individual vector queries to be executed. + # # + # @param [VectorQuery, Array] vector_queries vector queries/query to execute + # @param [Options::VectorSearch] options + def initialize(vector_queries, options = Options::VectorSearch::DEFAULT) + @vector_queries = vector_queries.respond_to?(:each) ? vector_queries : [vector_queries] + @options = options + end - # @return [Hash] - def to_h - {:match_none => nil} - end - end + # @api private + def to_backend + {vector_queries: @vector_queries.map(&:to_h).to_json}.merge(@options.to_backend) end + end - # @api volatile - class VectorSearch - # Constructs a +VectorSearch+ instance, which allows one or more individual vector queries to be executed. - # # - # @param [VectorQuery, Array] vector_queries vector queries/query to execute - # @param [Options::VectorSearch] options - def initialize(vector_queries, options = Options::VectorSearch::DEFAULT) - @vector_queries = vector_queries.respond_to?(:each) ? vector_queries : [vector_queries] - @options = options - end + # @api volatile + class VectorQuery + # @return [Integer, nil] + attr_accessor :num_candidates + + # @return [Float, nil] + attr_accessor :boost + + # Constructs a +VectorQuery+ instance + # + # @param [String] vector_field_name the document field that contains the vector. + # @param [Array] vector_query the vector query to run. + # + # @yieldparam [MatchPhraseQuery] self + def initialize(vector_field_name, vector_query) + @vector_field_name = vector_field_name + @vector_query = vector_query + + yield self if block_given? + end - # @api private - def to_backend - {vector_queries: @vector_queries.map(&:to_h).to_json}.merge(@options.to_backend) + # @api private + def to_h + if !num_candidates.nil? && num_candidates < 1 + raise Error::InvalidArgument, + "Number of candidates must be at least 1, #{num_candidates} given" end + + { + field: @vector_field_name, + vector: @vector_query, + k: num_candidates || 3, + boost: boost, + }.compact end - # @api volatile - class VectorQuery - # @return [Integer, nil] - attr_accessor :num_candidates + # @api private + def to_json(*args) + to_h.to_json(*args) + end + end - # @return [Float, nil] - attr_accessor :boost + class SearchSort + # @yieldparam [SearchSortScore] + # @return [SearchSortScore] + def self.score(&block) + SearchSortScore.new(&block) + end - # Constructs a +VectorQuery+ instance - # - # @param [String] vector_field_name the document field that contains the vector. - # @param [Array] vector_query the vector query to run. - # - # @yieldparam [MatchPhraseQuery] self - def initialize(vector_field_name, vector_query) - @vector_field_name = vector_field_name - @vector_query = vector_query + # @yieldparam [SearchSortId] + # @return [SearchSortScore] + def self.id(&block) + SearchSortId.new(&block) + end - yield self if block_given? - end + # @param [String] name field name + # @yieldparam [SearchSortField] + # @return [SearchSortField] + def self.field(name, &block) + SearchSortField.new(name, &block) + end - # @api private - def to_h - if !num_candidates.nil? && num_candidates < 1 - raise Error::InvalidArgument, - "Number of candidates must be at least 1, #{num_candidates} given" - end + # @param [String] name field name + # @param [Float] longitude + # @param [Float] latitude + # @yieldparam [SearchSortField] + # @return [SearchSortGeoDistance] + def self.geo_distance(name, longitude, latitude, &block) + SearchSortGeoDistance.new(name, longitude, latitude, &block) + end - { - field: @vector_field_name, - vector: @vector_query, - k: num_candidates || 3, - boost: boost, - }.compact + class SearchSortScore < SearchSort + # @return [Boolean] if descending order should be applied + attr_accessor :desc + + # @yieldparam [SearchSortScore] + def initialize + super + yield self if block_given? end # @api private def to_json(*args) - to_h.to_json(*args) + {by: :score, desc: desc}.to_json(*args) end end - class SearchSort - # @yieldparam [SearchSortScore] - # @return [SearchSortScore] - def self.score(&block) - SearchSortScore.new(&block) - end + class SearchSortId < SearchSort + # @return [Boolean] if descending order should be applied + attr_accessor :desc # @yieldparam [SearchSortId] - # @return [SearchSortScore] - def self.id(&block) - SearchSortId.new(&block) + def initialize + super + yield self if block_given? end - # @param [String] name field name - # @yieldparam [SearchSortField] - # @return [SearchSortField] - def self.field(name, &block) - SearchSortField.new(name, &block) + # @api private + def to_json(*args) + {by: :id, desc: desc}.to_json(*args) end + end - # @param [String] name field name - # @param [Float] longitude - # @param [Float] latitude - # @yieldparam [SearchSortField] - # @return [SearchSortGeoDistance] - def self.geo_distance(name, longitude, latitude, &block) - SearchSortGeoDistance.new(name, longitude, latitude, &block) - end + class SearchSortField < SearchSort + # @return [String] name of the field to sort by + attr_reader :field - class SearchSortScore < SearchSort - # @return [Boolean] if descending order should be applied - attr_accessor :desc + # @return [Boolean] if descending order should be applied + attr_accessor :desc - # @yieldparam [SearchSortScore] - def initialize - super - yield self if block_given? - end + # @return [:auto, :string, :number, :date] + attr_accessor :type - # @api private - def to_json(*args) - {by: :score, desc: desc}.to_json(*args) - end - end + # @return [:first, :last] where the documents with missing field should be placed + attr_accessor :missing - class SearchSortId < SearchSort - # @return [Boolean] if descending order should be applied - attr_accessor :desc + # @return [:default, :min, :max] + attr_accessor :mode - # @yieldparam [SearchSortId] - def initialize - super - yield self if block_given? - end + # @param [String] field the name of the filed for ordering + # @yieldparam [SearchSortField] + def initialize(field) + super() + @field = field + yield self if block_given? + end - # @api private - def to_json(*args) - {by: :id, desc: desc}.to_json(*args) - end + # @api private + def to_json(*args) + {by: :field, field: field, desc: desc, type: type, missing: missing, mode: mode}.to_json(*args) end + end - class SearchSortField < SearchSort - # @return [String] name of the field to sort by - attr_reader :field + class SearchSortGeoDistance < SearchSort + # @return [String] name of the field to sort by + attr_reader :field - # @return [Boolean] if descending order should be applied - attr_accessor :desc + # @return [Boolean] if descending order should be applied + attr_accessor :desc - # @return [:auto, :string, :number, :date] - attr_accessor :type + # @return [Float] + attr_reader :longitude - # @return [:first, :last] where the documents with missing field should be placed - attr_accessor :missing + # @return [Float] + attr_reader :latitude - # @return [:default, :min, :max] - attr_accessor :mode + # @return [:meters, :miles, :centimeters, :millimeters, :kilometers, :nauticalmiles, :feet, :yards, :inch] + attr_accessor :unit - # @param [String] field the name of the filed for ordering - # @yieldparam [SearchSortField] - def initialize(field) - super() - @field = field - yield self if block_given? - end + # @param [String] field field name + # @param [Float] longitude + # @param [Float] latitude + # @yieldparam [SearchSortGeoDistance] + def initialize(field, longitude, latitude) + super() + @field = field + @longitude = longitude + @latitude = latitude + yield self if block_given? + end - # @api private - def to_json(*args) - {by: :field, field: field, desc: desc, type: type, missing: missing, mode: mode}.to_json(*args) - end + # @api private + def to_json(*args) + {by: :geo_distance, field: field, desc: desc, location: [longitude, latitude], unit: unit}.to_json(*args) end + end + end - class SearchSortGeoDistance < SearchSort - # @return [String] name of the field to sort by - attr_reader :field + class SearchFacet + # @param [String] field_name + # @return [SearchFacetTerm] + def self.term(field_name, &block) + SearchFacetTerm.new(field_name, &block) + end - # @return [Boolean] if descending order should be applied - attr_accessor :desc + # @param [String] field_name + # @return [SearchFacetNumericRange] + def self.numeric_range(field_name, &block) + SearchFacetNumericRange.new(field_name, &block) + end - # @return [Float] - attr_reader :longitude + # @param [String] field_name + # @return [SearchFacetDateRange] + def self.date_range(field_name, &block) + SearchFacetDateRange.new(field_name, &block) + end - # @return [Float] - attr_reader :latitude + class SearchFacetTerm + # @return [String] + attr_reader :field - # @return [:meters, :miles, :centimeters, :millimeters, :kilometers, :nauticalmiles, :feet, :yards, :inch] - attr_accessor :unit + # @return [Integer] + attr_accessor :size - # @param [String] field field name - # @param [Float] longitude - # @param [Float] latitude - # @yieldparam [SearchSortGeoDistance] - def initialize(field, longitude, latitude) - super() - @field = field - @longitude = longitude - @latitude = latitude - yield self if block_given? - end + # @param [String] field name of the field + def initialize(field) + @field = field + yield self if block_given? + end - # @api private - def to_json(*args) - {by: :geo_distance, field: field, desc: desc, location: [longitude, latitude], unit: unit}.to_json(*args) - end + # @api private + def to_json(*args) + {field: field, size: size}.to_json(*args) end end - class SearchFacet - # @param [String] field_name - # @return [SearchFacetTerm] - def self.term(field_name, &block) - SearchFacetTerm.new(field_name, &block) - end + class SearchFacetNumericRange + # @return [String] + attr_reader :field - # @param [String] field_name - # @return [SearchFacetNumericRange] - def self.numeric_range(field_name, &block) - SearchFacetNumericRange.new(field_name, &block) - end + # @return [Integer] + attr_accessor :size - # @param [String] field_name - # @return [SearchFacetDateRange] - def self.date_range(field_name, &block) - SearchFacetDateRange.new(field_name, &block) + # @param [String] field name of the field + def initialize(field) + @field = field + @ranges = [] + yield self if block_given? end - class SearchFacetTerm - # @return [String] - attr_reader :field - - # @return [Integer] - attr_accessor :size - - # @param [String] field name of the field - def initialize(field) - @field = field - yield self if block_given? - end - - # @api private - def to_json(*args) - {field: field, size: size}.to_json(*args) - end + # @param [String] name the name of the range + # @param [Integer, Float, nil] min lower bound of the range (pass +nil+ if there is no lower bound) + # @param [Integer, Float, nil] max upper bound of the range (pass +nil+ if there is no upper bound) + def add(name, min, max) + @ranges.append({name: name, min: min, max: max}) end - class SearchFacetNumericRange - # @return [String] - attr_reader :field - - # @return [Integer] - attr_accessor :size - - # @param [String] field name of the field - def initialize(field) - @field = field - @ranges = [] - yield self if block_given? - end - - # @param [String] name the name of the range - # @param [Integer, Float, nil] min lower bound of the range (pass +nil+ if there is no lower bound) - # @param [Integer, Float, nil] max upper bound of the range (pass +nil+ if there is no upper bound) - def add(name, min, max) - @ranges.append({name: name, min: min, max: max}) - end - - # @api private - def to_json(*args) - {field: field, size: size, numeric_ranges: @ranges}.to_json(*args) - end + # @api private + def to_json(*args) + {field: field, size: size, numeric_ranges: @ranges}.to_json(*args) end + end - class SearchFacetDateRange - # @return [String] - attr_reader :field + class SearchFacetDateRange + # @return [String] + attr_reader :field - # @return [Integer] - attr_accessor :size + # @return [Integer] + attr_accessor :size - # @param [String] field name of the field - def initialize(field) - super() - @field = field - @ranges = [] - yield self if block_given? - end + # @param [String] field name of the field + def initialize(field) + super() + @field = field + @ranges = [] + yield self if block_given? + end - DATE_FORMAT_RFC3339 = "%Y-%m-%dT%H:%M:%S%:z".freeze + DATE_FORMAT_RFC3339 = "%Y-%m-%dT%H:%M:%S%:z".freeze - # @param [String] name the name of the range - # @param [Time, String, nil] start_time lower bound of the range (pass +nil+ if there is no lower bound) - # @param [Time, String, nil] end_time lower bound of the range (pass +nil+ if there is no lower bound) - def add(name, start_time, end_time) - start_time = start_time.strftime(DATE_FORMAT_RFC3339) if start_time.respond_to?(:strftime) - end_time = end_time.strftime(DATE_FORMAT_RFC3339) if end_time.respond_to?(:strftime) - @ranges.append({name: name, start: start_time, end: end_time}) - end + # @param [String] name the name of the range + # @param [Time, String, nil] start_time lower bound of the range (pass +nil+ if there is no lower bound) + # @param [Time, String, nil] end_time lower bound of the range (pass +nil+ if there is no lower bound) + def add(name, start_time, end_time) + start_time = start_time.strftime(DATE_FORMAT_RFC3339) if start_time.respond_to?(:strftime) + end_time = end_time.strftime(DATE_FORMAT_RFC3339) if end_time.respond_to?(:strftime) + @ranges.append({name: name, start: start_time, end: end_time}) + end - # @api private - def to_json(*args) - {field: field, size: size, date_ranges: @ranges}.to_json(*args) - end + # @api private + def to_json(*args) + {field: field, size: size, date_ranges: @ranges}.to_json(*args) end end + end - class SearchRowLocation - # @return [String] - attr_accessor :field + class SearchRowLocation + # @return [String] + attr_accessor :field - # @return [String] - attr_accessor :term + # @return [String] + attr_accessor :term - # @return [Integer] the position of the term within the field, starting at 1 - attr_accessor :position + # @return [Integer] the position of the term within the field, starting at 1 + attr_accessor :position - # @return [Integer] start byte offset of the term in the field - attr_accessor :start_offset + # @return [Integer] start byte offset of the term in the field + attr_accessor :start_offset - # @return [Integer] end byte offset of the term in the field - attr_accessor :end_offset + # @return [Integer] end byte offset of the term in the field + attr_accessor :end_offset - # @return [Array] the positions of the term within any elements. - attr_accessor :array_positions + # @return [Array] the positions of the term within any elements. + attr_accessor :array_positions - # @yieldparam [SearchRowLocation] self - def initialize - yield self if block_given? - end + # @yieldparam [SearchRowLocation] self + def initialize + yield self if block_given? end + end - class SearchRowLocations - # Lists all locations (any field, any term) - # - # @return [Array] - def get_all - @locations - end + class SearchRowLocations + # Lists all locations (any field, any term) + # + # @return [Array] + def get_all + @locations + end - # Lists all locations for a given field (any term) - # - # @return [Array] - def get_for_field(name) - @locations.select { |location| location.field == name } - end + # Lists all locations for a given field (any term) + # + # @return [Array] + def get_for_field(name) + @locations.select { |location| location.field == name } + end - # Lists all locations for a given field and term - # - # @return [Array] - def get_for_field_and_term(name, term) - @locations.select { |location| location.field == name && location.term == term } - end + # Lists all locations for a given field and term + # + # @return [Array] + def get_for_field_and_term(name, term) + @locations.select { |location| location.field == name && location.term == term } + end - # Lists the fields in this location - # - # @return [Array] - def fields - @locations.map(&:field).uniq - end + # Lists the fields in this location + # + # @return [Array] + def fields + @locations.map(&:field).uniq + end - # Lists all terms in this locations, considering all fields - # - # @return [Array] - def terms - @locations.map(&:term).uniq - end + # Lists all terms in this locations, considering all fields + # + # @return [Array] + def terms + @locations.map(&:term).uniq + end - # Lists the terms for a given field - # - # @return [Array] - def terms_for_field(name) - get_for_field(name).map(&:term).uniq - end + # Lists the terms for a given field + # + # @return [Array] + def terms_for_field(name) + get_for_field(name).map(&:term).uniq + end - # @param [Array] locations - def initialize(locations) - super() - @locations = locations - end + # @param [Array] locations + def initialize(locations) + super() + @locations = locations end + end - # An individual facet result has both metadata and details, as each facet can define ranges into which results are categorized - class SearchFacetResult - # @return [String] - attr_accessor :name + # An individual facet result has both metadata and details, as each facet can define ranges into which results are categorized + class SearchFacetResult + # @return [String] + attr_accessor :name - # @return [String] - attr_accessor :field + # @return [String] + attr_accessor :field - # @return [Integer] - attr_accessor :total + # @return [Integer] + attr_accessor :total - # @return [Integer] - attr_accessor :missing + # @return [Integer] + attr_accessor :missing - # @return [Integer] - attr_accessor :other + # @return [Integer] + attr_accessor :other - class TermFacetResult < SearchFacetResult - # @return [Array] - attr_accessor :terms + class TermFacetResult < SearchFacetResult + # @return [Array] + attr_accessor :terms - def type - :term_facet - end + def type + :term_facet + end - # @yieldparam [TermFacetResult] self - def initialize - super - yield self if block_given? - end + # @yieldparam [TermFacetResult] self + def initialize + super + yield self if block_given? + end - class TermFacet - # @return [String] - attr_reader :term + class TermFacet + # @return [String] + attr_reader :term - # @return [Integer] - attr_reader :count + # @return [Integer] + attr_reader :count - def initialize(term, count) - super() - @term = term - @count = count - end + def initialize(term, count) + super() + @term = term + @count = count end end + end - class DateRangeFacetResult < SearchFacetResult - # @return [Array] - attr_accessor :date_ranges + class DateRangeFacetResult < SearchFacetResult + # @return [Array] + attr_accessor :date_ranges - def type - :date_range_facet - end + def type + :date_range_facet + end - # @yieldparam [DateRangeFacetResult] self - def initialize - super - yield self if block_given? - end + # @yieldparam [DateRangeFacetResult] self + def initialize + super + yield self if block_given? + end - class DateRangeFacet - # @return [String] - attr_reader :name + class DateRangeFacet + # @return [String] + attr_reader :name - # @return [Integer] - attr_reader :count + # @return [Integer] + attr_reader :count - # @return [String] - attr_reader :start_time + # @return [String] + attr_reader :start_time - # @return [String] - attr_reader :end_time + # @return [String] + attr_reader :end_time - def initialize(name, count, start_time, end_time) - @name = name - @count = count - @start_time = start_time - @end_time = end_time - end + def initialize(name, count, start_time, end_time) + @name = name + @count = count + @start_time = start_time + @end_time = end_time end end + end - class NumericRangeFacetResult < SearchFacetResult - # @return [Array] - attr_accessor :numeric_ranges + class NumericRangeFacetResult < SearchFacetResult + # @return [Array] + attr_accessor :numeric_ranges - def type - :numeric_range_facet - end + def type + :numeric_range_facet + end - # @yieldparam [NumericRangeFacetResult] self - def initialize - super - yield self if block_given? - end + # @yieldparam [NumericRangeFacetResult] self + def initialize + super + yield self if block_given? + end - class NumericRangeFacet - # @return [String] - attr_reader :name + class NumericRangeFacet + # @return [String] + attr_reader :name - # @return [Integer] - attr_reader :count + # @return [Integer] + attr_reader :count - # @return [Integer, Float, nil] - attr_reader :min + # @return [Integer, Float, nil] + attr_reader :min - # @return [Integer, Float, nil] - attr_reader :max + # @return [Integer, Float, nil] + attr_reader :max - def initialize(name, count, min, max) - @name = name - @count = count - @min = min - @max = max - end + def initialize(name, count, min, max) + @name = name + @count = count + @min = min + @max = max end end end + end - class SearchRow - # @return [String] name of the index - attr_accessor :index + class SearchRow + # @return [String] name of the index + attr_accessor :index - # @return [String] document identifier - attr_accessor :id + # @return [String] document identifier + attr_accessor :id - # @return [Float] - attr_accessor :score + # @return [Float] + attr_accessor :score - # @return [SearchRowLocations] - attr_accessor :locations + # @return [SearchRowLocations] + attr_accessor :locations - # @return [Hash] - attr_accessor :explanation + # @return [Hash] + attr_accessor :explanation - # @return [Hash Array>] - attr_accessor :fragments + # @return [Hash Array>] + attr_accessor :fragments - # @return [JsonTranscoder] transcoder to use for the fields - attr_accessor :transcoder + # @return [JsonTranscoder] transcoder to use for the fields + attr_accessor :transcoder - def fields - @transcoder.decode(@fields, :json) if @fields && @transcoder - end + def fields + @transcoder.decode(@fields, :json) if @fields && @transcoder + end - # @yieldparam [SearchRow] self - def initialize - @fields = nil - yield self if block_given? - end + # @yieldparam [SearchRow] self + def initialize + @fields = nil + yield self if block_given? end + end - class SearchMetrics - # @return [Integer] time spent executing the query (in milliseconds) - attr_accessor :took + class SearchMetrics + # @return [Integer] time spent executing the query (in milliseconds) + attr_accessor :took - # @return [Integer] - attr_accessor :total_rows + # @return [Integer] + attr_accessor :total_rows - # @return [Float] - attr_accessor :max_score + # @return [Float] + attr_accessor :max_score - # @return [Integer] - attr_accessor :success_partition_count + # @return [Integer] + attr_accessor :success_partition_count - # @return [Integer] - attr_accessor :error_partition_count + # @return [Integer] + attr_accessor :error_partition_count - # @return [Integer] - def total_partition_count - success_partition_count + error_partition_count - end + # @return [Integer] + def total_partition_count + success_partition_count + error_partition_count end + end - class SearchMetaData - # @return [SearchMetrics] - attr_accessor :metrics + class SearchMetaData + # @return [SearchMetrics] + attr_accessor :metrics - # @return [Hash String>] - attr_accessor :errors + # @return [Hash String>] + attr_accessor :errors - # @yieldparam [SearchMetaData] self - def initialize - @metrics = SearchMetrics.new - yield self if block_given? - end + # @yieldparam [SearchMetaData] self + def initialize + @metrics = SearchMetrics.new + yield self if block_given? end + end - class SearchResult - # @return [Array] - attr_accessor :rows + class SearchResult + # @return [Array] + attr_accessor :rows - # @return [Hash SearchFacetResult>] - attr_accessor :facets + # @return [Hash SearchFacetResult>] + attr_accessor :facets - # @return [SearchMetaData] - attr_accessor :meta_data + # @return [SearchMetaData] + attr_accessor :meta_data - def success? - meta_data.errors.nil? || meta_data.errors.empty? - end + def success? + meta_data.errors.nil? || meta_data.errors.empty? + end - # @yieldparam [SearchResult] self - def initialize - yield self if block_given? - end + # @yieldparam [SearchResult] self + def initialize + yield self if block_given? end end + + class Cluster + SearchQuery = Couchbase::SearchQuery + SearchSort = Couchbase::SearchSort + SearchFacet = Couchbase::SearchFacet + SearchRowLocation = Couchbase::SearchRowLocation + SearchRowLocations = Couchbase::SearchRowLocations + SearchFacetResult = Couchbase::SearchFacetResult + SearchRow = Couchbase::SearchRow + SearchResult = Couchbase::SearchResult + SearchMetaData = Couchbase::SearchMetaData + SearchMetrics = Couchbase::SearchMetrics + end end diff --git a/test/scope_search_index_manager_test.rb b/test/scope_search_index_manager_test.rb new file mode 100644 index 00000000..6f1e649e --- /dev/null +++ b/test/scope_search_index_manager_test.rb @@ -0,0 +1,142 @@ +# Copyright 2024. Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "test_helper" + +module Couchbase + class ScopeSearchIndexManagerTest < Minitest::Test + include TestUtilities + + def setup + unless env.server_version.supports_scoped_search_indexes? + skip("skipped for (#{env.server_version}) as the ScopeSearchIndexManagerTest requires scoped search index support") + end + + @index_names = [] + + connect + @bucket = @cluster.bucket(env.bucket) + @scope = @bucket.default_scope + @mgr = @scope.search_indexes + end + + def teardown + @index_names.each do |name| + @mgr.drop_index(name) + rescue StandardError + Error::IndexNotFound + end + disconnect + end + + def get_search_index_name + name = uniq_id(:scope_idx) + @index_names << name + name + end + + def test_index_not_found + [ + [:drop_index, ["test-index"]], + [:get_indexed_documents_count, ["test-index"]], + [:pause_ingest, ["test-index"]], + [:resume_ingest, ["test-index"]], + [:allow_querying, ["test-index"]], + [:disallow_querying, ["test-index"]], + [:freeze_plan, ["test-index"]], + [:unfreeze_plan, ["test-index"]], + ].each do |method_name, args| + assert_raises(Error::IndexNotFound, "Expected #{method_name} to raise IndexNotFound") do + @mgr.public_send(method_name, *args) + end + end + end + + def test_index_crud + index_name = uniq_id(:scope_idx) + + @mgr.upsert_index( + Management::SearchIndex.new do |idx| + idx.name = index_name + idx.source_name = env.bucket + end + ) + + # Upsert requires a UUID + assert_raises(Error::IndexExists) do + @mgr.upsert_index( + Management::SearchIndex.new do |idx| + idx.name = index_name + idx.source_name = env.bucket + end + ) + end + + idx = @mgr.get_index(index_name) + + assert_equal index_name, idx.name + assert_equal "default", idx.source_name + + @mgr.upsert_index(idx) + + indexes = @mgr.get_all_indexes + + refute_empty indexes + + @mgr.drop_index(index_name) + + assert_raises(Error::IndexNotFound) do + @mgr.drop_index(index_name) + end + end + end + + class ScopeSearchIndexManagerNotSupportedTest < Minitest::Test + include TestUtilities + + def setup + skip("Test requires not having scoped search index support") if env.server_version.supports_scoped_search_indexes? + + connect + @bucket = @cluster.bucket(env.bucket) + @scope = @bucket.default_scope + @mgr = @scope.search_indexes + end + + def teardown + disconnect + end + + def test_feature_not_available + [ + [:get_index, ["test-index"]], + [:get_all_indexes, []], + [:upsert_index, [Management::SearchIndex.new { |idx| idx.name = "test-index" }]], + [:drop_index, ["test-index"]], + [:get_indexed_documents_count, ["test-index"]], + [:pause_ingest, ["test-index"]], + [:resume_ingest, ["test-index"]], + [:allow_querying, ["test-index"]], + [:disallow_querying, ["test-index"]], + [:freeze_plan, ["test-index"]], + [:unfreeze_plan, ["test-index"]], + [:analyze_document, ["test-index", {foo: "bar"}]], + ].each do |method_name, args| + assert_raises(Error::FeatureNotAvailable, "Expected #{method_name} to raise FeatureNotAvailable") do + @mgr.public_send(method_name, *args) + end + end + end + end +end diff --git a/test/search_test.rb b/test/search_test.rb index 7ad01793..947cced1 100644 --- a/test/search_test.rb +++ b/test/search_test.rb @@ -171,21 +171,21 @@ def test_search_request_backend_encoding end vector_queries = [ - Cluster::VectorQuery.new("cityVector", vec1) do |q| + VectorQuery.new("cityVector", vec1) do |q| q.num_candidates = 3 q.boost = 0.7 end, - Cluster::VectorQuery.new("cityVector", vec2) do |q| + VectorQuery.new("cityVector", vec2) do |q| q.num_candidates = 2 q.boost = 0.3 end, ] - vector_search = Cluster::VectorSearch.new(vector_queries, Options::VectorSearch.new(vector_query_combination: :or)) + vector_search = VectorSearch.new(vector_queries, Options::VectorSearch.new(vector_query_combination: :or)) # Both requests should be equivalent requests = [ - Cluster::SearchRequest.new(vector_search).search_query(query), - Cluster::SearchRequest.new(query).vector_search(vector_search), + SearchRequest.new(vector_search).search_query(query), + SearchRequest.new(query).vector_search(vector_search), ] requests.each do |request| enc_query, enc_request = request.to_backend @@ -200,18 +200,18 @@ def test_search_request_invalid_argument vec1 = [-0.00810323283, 0.0727998167, 0.0211895034, -0.0254271757] vec2 = [-0.005610323283, 0.023427998167, 0.0132511895034, 0.03466271757] - query = Cluster::SearchQuery.prefix("S") + query = SearchQuery.prefix("S") vector_queries = [ - Cluster::VectorQuery.new("cityVector", vec1), - Cluster::VectorQuery.new("cityVector", vec2), + VectorQuery.new("cityVector", vec1), + VectorQuery.new("cityVector", vec2), ] - vector_search = Cluster::VectorSearch.new(vector_queries, Options::VectorSearch.new(vector_query_combination: :or)) + vector_search = VectorSearch.new(vector_queries, Options::VectorSearch.new(vector_query_combination: :or)) invalid_initializations = [ - proc { Cluster::SearchRequest.new(vector_search).vector_search(vector_search) }, - proc { Cluster::SearchRequest.new(query).vector_search(vector_search).vector_search(vector_search) }, - proc { Cluster::SearchRequest.new(query).search_query(query) }, - proc { Cluster::SearchRequest.new(vector_search).search_query(query).search_query(query) }, + proc { SearchRequest.new(vector_search).vector_search(vector_search) }, + proc { SearchRequest.new(query).vector_search(vector_search).vector_search(vector_search) }, + proc { SearchRequest.new(query).search_query(query) }, + proc { SearchRequest.new(vector_search).search_query(query).search_query(query) }, ] invalid_initializations.each do |it| @@ -220,7 +220,7 @@ def test_search_request_invalid_argument end def test_vector_query_invalid_candidate_number - vector_query = Cluster::VectorQuery.new("foo", [-1.1, 1.2]) do |q| + vector_query = VectorQuery.new("foo", [-1.1, 1.2]) do |q| q.num_candidates = 0 end @@ -230,10 +230,10 @@ def test_vector_query_invalid_candidate_number end def test_vector_search_query_defaults_to_match_none - vector_search = Cluster::VectorSearch.new(Cluster::VectorQuery.new("foo", [-1.1, 1.2])) - enc_query, = Cluster::SearchRequest.new(vector_search).to_backend + vector_search = VectorSearch.new(VectorQuery.new("foo", [-1.1, 1.2])) + enc_query, = SearchRequest.new(vector_search).to_backend - assert_equal Cluster::SearchQuery.match_none.to_json, enc_query + assert_equal SearchQuery.match_none.to_json, enc_query end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index b102473f..9550122d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -114,6 +114,10 @@ def supports_update_collection_max_expiry? def supports_collection_max_expiry_set_to_no_expiry? @version >= Gem::Version.create("7.6.0") end + + def supports_scoped_search_indexes? + @version >= Gem::Version.create("7.5.0") + end end require "couchbase"