From 92054b2bbbfaa42f6dd1ac11c9e3ea118d4a26f1 Mon Sep 17 00:00:00 2001 From: Sebastien Savater Date: Mon, 27 May 2024 11:00:23 +0200 Subject: [PATCH] Refactor configuration --- docs/documentation/searches/filters/date.md | 41 ++-- docs/getting_started.md | 68 ++++++- docs/index.md | 22 ++- lib/caoutsearch.rb | 25 +-- .../{config => concerns}/client.rb | 4 +- .../{config => concerns}/mappings.rb | 2 +- .../{config => concerns}/settings.rb | 6 +- lib/caoutsearch/configuration.rb | 65 +++++++ lib/caoutsearch/index/base.rb | 6 +- lib/caoutsearch/instrumentation/index.rb | 2 +- lib/caoutsearch/instrumentation/search.rb | 2 +- lib/caoutsearch/search/base.rb | 6 +- lib/caoutsearch/testing/mock_requests.rb | 4 +- spec/caoutsearch/configuration_spec.rb | 176 ++++++++++++++++++ 14 files changed, 367 insertions(+), 62 deletions(-) rename lib/caoutsearch/{config => concerns}/client.rb (63%) rename lib/caoutsearch/{config => concerns}/mappings.rb (98%) rename lib/caoutsearch/{config => concerns}/settings.rb (80%) create mode 100644 lib/caoutsearch/configuration.rb create mode 100644 spec/caoutsearch/configuration_spec.rb diff --git a/docs/documentation/searches/filters/date.md b/docs/documentation/searches/filters/date.md index 3931b10..76ae821 100644 --- a/docs/documentation/searches/filters/date.md +++ b/docs/documentation/searches/filters/date.md @@ -2,7 +2,8 @@ title: Date --- -For a date filter defined like this: +Register a date filter like this: + ```ruby class ArticleSearch < Caoutsearch::Search::Base ... @@ -11,7 +12,8 @@ class ArticleSearch < Caoutsearch::Search::Base end ``` -You can now search the matching index with the `published_on` criterion: +You can now search with the matching property: + ```ruby Article.search(published_on: Date.today) ``` @@ -29,7 +31,27 @@ and the following query will be generated to send to elasticsearch: } ``` -The date filter accepts multiple types of arguments : +Various formats for times & dates are accepted: +```ruby +"2022-10-11" +Date.today +Time.zone.now +``` + +It also supports [Elasticsearch Date Math](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math): +```ruby +"now-1h" +"now+2w/d" +``` + +It also accepts ranges (closed or open) in various formats: `Range`, `Hash` or `Array` +```ruby +..Date.new(2022, 12, 25) +{ less_than: "2022-12-25" } +[["now-1w/d", "now/d"]] +``` + +Examples: ```ruby # Search for articles published on a date: @@ -52,16 +74,3 @@ Article.search(published_on: { greater_than: "2022-12-25", less_than: "2023-12-2 Article.search(published_on: Date.new(2022, 12, 25)..Date.new(2023, 12, 25)) Article.search(published_on: [["now-1w/d", "now/d"]]) ``` - -Dates of various formats are handled: -```ruby -"2022-10-11" -Date.today -Time.zone.now -``` - -We also support elasticsearch's date math -```ruby -"now-1h" -"now+2w/d" -``` \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index 4b424b6..600a1ce 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,8 +1,8 @@ ## Prerequisites Caoutsearch requires at least : -* Ruby 2.7.x -* Rails 6.x +* Ruby >= 2.7.x +* Rails >= 6.x * Elasticsearch 8.x ## Installation @@ -15,4 +15,66 @@ bundle add caoutsearch ## Configuration -TODO \ No newline at end of file +Few options are available to configure Caoutsearch: + +```ruby +Caoutsearch.configure do |config| + # Configure how to connect to Elasticsearch. + # + # `Elasticsearch::Client` is provided by the elasticsearch gem. + # See https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/connecting.html + # + config.client = Elasticsearch::Client.new( + host: "https://my-elasticsearch-host.example", + retry_on_failure: true, + request_timeout: 30 + ) + + # Set defaults settings to apply to every indexes: + # + # See https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings + # + config.settings = { + number_of_shards: 5, + number_of_replicas: 2, + analysis: { + analyzer: { + words: { type: :custom, tokenizer: :standard, filter: %i[lowercase word_length] } + }, + normalizer: { + lowercase: { type: :custom, filter: %i[lowercase] }, + }, + filter: { + word_length: { type: :length, min: 1 }, + } + } + } + + # Instrument Elasticsearch requests into your Rails logs. + # There are three different output: + # - 'full' - Log the full request + # - 'truncated' - Log only the 200 first character of the request + # - 'amazing_print' - Pretty print the request body. + # + # Instrumentation is not enabled by default. + # Without argument, 'full' format is used by default. + # You need to install the amazing_print gem to use the corresponding format. + # + config.instrument + + # You can also define alternative configurations to indexes & searches: + # + config.index do |index| + config.instrument :truncated + end + + config.search do |search| + search.client = Elasticsearch::Client.new( + host: "https://my-elasticsearch-host.example", + request_timeout: 10 + ) + + config.instrument :amazing_print + end +end +``` diff --git a/docs/index.md b/docs/index.md index e8e8675..c5aad1d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,18 +25,23 @@ Depending on your search scenarios, they may better suite your needs. Caoutsearch let you create `Index` and `Search` classes to manipulate your data : ```ruby +class Article < ActiveRecord::Base + include Caoutsearch::Model + + index_with ArticleIndex + search_with ArticleSearch +end + class ArticleIndex < Caoutsearch::Index::Base property :title property :published_on property :tags def tags - records.tags.public.map(&:to_s) + record.tags.public.map(&:to_s) end end -``` -```ruby class ArticleSearch < Caoutsearch::Search::Base filter :title, as: :match filter :published_on, as: :date @@ -53,14 +58,13 @@ class ArticleSearch < Caoutsearch::Search::Base end ``` -You can then index your records +You can then index & search through your records: ```ruby -ArticleIndex.reindex +Article.reindex ``` - -Or search through them: - ```ruby -ArticleSearch.search(published_on: [["now-1y", nil]]).aggregate(:popular_tags) +Article.search(published_on: [["now-1y", nil]]).aggregate(:popular_tags) ``` + +But that's just the beginning... \ No newline at end of file diff --git a/lib/caoutsearch.rb b/lib/caoutsearch.rb index 8b12f3a..8191e28 100644 --- a/lib/caoutsearch.rb +++ b/lib/caoutsearch.rb @@ -16,28 +16,17 @@ module Caoutsearch class << self - attr_writer :client - - def client - @client ||= Elasticsearch::Client.new - end - - def settings - @settings ||= Caoutsearch::Settings.new({}) - end - - def settings=(settings) - @settings = Caoutsearch::Settings.new(settings) + def config + @config ||= Configuration.new end + alias_method :configuration, :config - def instrument!(**options) - @instrumentation_options = options - Caoutsearch::Instrumentation::Index.attach_to :caoutsearch_index if options[:index] - Caoutsearch::Instrumentation::Search.attach_to :caoutsearch_search if options[:search] + def configure + yield(configuration) end - def instrumentation_options - @instrumentation_options ||= {} + def reset_config + @config = Configuration.new end end end diff --git a/lib/caoutsearch/config/client.rb b/lib/caoutsearch/concerns/client.rb similarity index 63% rename from lib/caoutsearch/config/client.rb rename to lib/caoutsearch/concerns/client.rb index fafe8da..4f21e72 100644 --- a/lib/caoutsearch/config/client.rb +++ b/lib/caoutsearch/concerns/client.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module Caoutsearch - module Config + module Concerns module Client extend ActiveSupport::Concern included do - class_attribute :client, default: Caoutsearch.client + class_attribute :client, default: Caoutsearch.config.client end end end diff --git a/lib/caoutsearch/config/mappings.rb b/lib/caoutsearch/concerns/mappings.rb similarity index 98% rename from lib/caoutsearch/config/mappings.rb rename to lib/caoutsearch/concerns/mappings.rb index 9f6bf63..cfad187 100644 --- a/lib/caoutsearch/config/mappings.rb +++ b/lib/caoutsearch/concerns/mappings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Caoutsearch - module Config + module Concerns module Mappings extend ActiveSupport::Concern diff --git a/lib/caoutsearch/config/settings.rb b/lib/caoutsearch/concerns/settings.rb similarity index 80% rename from lib/caoutsearch/config/settings.rb rename to lib/caoutsearch/concerns/settings.rb index 3500746..3e7eb46 100644 --- a/lib/caoutsearch/config/settings.rb +++ b/lib/caoutsearch/concerns/settings.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module Caoutsearch - module Config + module Concerns module Settings extend ActiveSupport::Concern included do - delegate :mappings, to: :class + delegate :settings, to: :class end class_methods do @@ -21,7 +21,7 @@ def settings=(settings) protected def default_settings - Caoutsearch.settings.to_hash.dup + Caoutsearch.config.settings.to_hash.dup end end end diff --git a/lib/caoutsearch/configuration.rb b/lib/caoutsearch/configuration.rb new file mode 100644 index 0000000..aaa54ad --- /dev/null +++ b/lib/caoutsearch/configuration.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Caoutsearch + class Configuration + DEFAULT_INSTRUMENTATION_FORMAT = "full" + + attr_reader :client, :settings, :instrumentation_format + + def initialize + self.client = Elasticsearch::Client.new + self.settings = {} + end + + def client=(client) + @client = client + + index.client = client + search.client = client + end + + def settings=(settings) + settings = Caoutsearch::Settings.new(settings) if settings.is_a?(Hash) + @settings = settings + end + + def instrument(format = DEFAULT_INSTRUMENTATION_FORMAT) + @instrumentation_format = format.to_s + + index.instrument(format) + search.instrument(format) + end + + def index + @index ||= IndexConfig.new + yield(@index) if block_given? + @index + end + + def search + @search ||= SearchConfig.new + yield(@search) if block_given? + @search + end + + class IndexConfig + attr_accessor :client + attr_reader :instrumentation_format + + def instrument(format = DEFAULT_INSTRUMENTATION_FORMAT) + @instrumentation_format = format.to_s + Caoutsearch::Instrumentation::Index.attach_to :caoutsearch_index + end + end + + class SearchConfig + attr_accessor :client + attr_reader :instrumentation_format + + def instrument(format = DEFAULT_INSTRUMENTATION_FORMAT) + @instrumentation_format = format.to_s + Caoutsearch::Instrumentation::Search.attach_to :caoutsearch_search + end + end + end +end diff --git a/lib/caoutsearch/index/base.rb b/lib/caoutsearch/index/base.rb index a10dc3c..4cf4058 100644 --- a/lib/caoutsearch/index/base.rb +++ b/lib/caoutsearch/index/base.rb @@ -3,9 +3,9 @@ module Caoutsearch module Index class Base - include Caoutsearch::Config::Client - include Caoutsearch::Config::Mappings - include Caoutsearch::Config::Settings + include Caoutsearch::Concerns::Client + include Caoutsearch::Concerns::Mappings + include Caoutsearch::Concerns::Settings include Caoutsearch::Index::Document include Caoutsearch::Index::Indice diff --git a/lib/caoutsearch/instrumentation/index.rb b/lib/caoutsearch/instrumentation/index.rb index 87db7da..ed22198 100644 --- a/lib/caoutsearch/instrumentation/index.rb +++ b/lib/caoutsearch/instrumentation/index.rb @@ -50,7 +50,7 @@ def reindex(event) private def log_request_format - Caoutsearch.instrumentation_options[:index] + Caoutsearch.config.index.instrumentation_format end end end diff --git a/lib/caoutsearch/instrumentation/search.rb b/lib/caoutsearch/instrumentation/search.rb index e0f5183..9bfb5ba 100644 --- a/lib/caoutsearch/instrumentation/search.rb +++ b/lib/caoutsearch/instrumentation/search.rb @@ -43,7 +43,7 @@ def delete(event) private def log_request_format - Caoutsearch.instrumentation_options[:search] + Caoutsearch.config.search.instrumentation_format end end end diff --git a/lib/caoutsearch/search/base.rb b/lib/caoutsearch/search/base.rb index ecdc79e..7a56104 100644 --- a/lib/caoutsearch/search/base.rb +++ b/lib/caoutsearch/search/base.rb @@ -3,9 +3,9 @@ module Caoutsearch module Search class Base - include Caoutsearch::Config::Client - include Caoutsearch::Config::Mappings - include Caoutsearch::Config::Settings + include Caoutsearch::Concerns::Client + include Caoutsearch::Concerns::Mappings + include Caoutsearch::Concerns::Settings include Caoutsearch::Search::BatchMethods include Caoutsearch::Search::Batch::Scroll diff --git a/lib/caoutsearch/testing/mock_requests.rb b/lib/caoutsearch/testing/mock_requests.rb index f0adb66..321615b 100644 --- a/lib/caoutsearch/testing/mock_requests.rb +++ b/lib/caoutsearch/testing/mock_requests.rb @@ -92,7 +92,7 @@ def stub_elasticsearch_batching_requests(index_name, hits = [], keep_alive: "1m" def elasticsearch_client_host @client_host ||= begin - transport = Caoutsearch.client.transport + transport = Caoutsearch.config.client.transport transport.__full_url(transport.hosts[0]) end end @@ -120,7 +120,7 @@ def stub_elasticsearch_validation_request ) if Gem::Version.new(Elasticsearch::VERSION) >= Gem::Version.new("8.9.0") - Caoutsearch.client.perform_request("GET", "/") + Caoutsearch.config.client.perform_request("GET", "/") end stubbed_request diff --git a/spec/caoutsearch/configuration_spec.rb b/spec/caoutsearch/configuration_spec.rb new file mode 100644 index 0000000..6feb429 --- /dev/null +++ b/spec/caoutsearch/configuration_spec.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Caoutsearch::Configuration do + context "with default config" do + it "sets a default client" do + expect(Caoutsearch.config.client).to be_a(Elasticsearch::Client) + end + + it "sets a default client for indexes" do + expect(Caoutsearch.config.index.client) + .to be_a(Elasticsearch::Client) + .and eq(Caoutsearch.config.client) + end + + it "sets a default client for searches" do + expect(Caoutsearch.config.search.client) + .to be_a(Elasticsearch::Client) + .and eq(Caoutsearch.config.client) + end + + it "sets default settings", :aggregate_failures do + expect(Caoutsearch.config.settings).to be_a(Caoutsearch::Settings) + expect(Caoutsearch.config.settings.to_hash).to eq({}) + end + + it "set indexes instrumentation" do + expect(Caoutsearch.config.index.instrumentation_format).to be_nil + end + + it "set searches instrumentation" do + expect(Caoutsearch.config.search.instrumentation_format).to be_nil + end + end + + context "with only one custom configuration" do + let(:client) { Elasticsearch::Client.new(host: "https://my-elasticsearch-host.example") } + + before do + Caoutsearch.configure do |config| + config.client = client + config.settings = { + number_of_shards: 5, + number_of_replicas: 2 + } + + config.instrument + end + end + + after do + Caoutsearch.reset_config + end + + it "sets the default client" do + expect(Caoutsearch.config.client).to be(client) + end + + it "sets the default client for indexes" do + expect(Caoutsearch.config.index.client).to be(client) + end + + it "sets the default client for searches" do + expect(Caoutsearch.config.search.client).to be(client) + end + + it "sets the default settings", :aggregate_failures do + expect(Caoutsearch.config.settings).to be_a(Caoutsearch::Settings) + expect(Caoutsearch.config.settings.to_hash).to eq({ + number_of_shards: 5, + number_of_replicas: 2 + }) + end + + it "set indexes instrumentation" do + expect(Caoutsearch.config.index.instrumentation_format).to eq("full") + end + + it "set searches instrumentation" do + expect(Caoutsearch.config.search.instrumentation_format).to eq("full") + end + end + + context "with a custom index configuration" do + let(:client) { Elasticsearch::Client.new(host: "https://my-elasticsearch-host.example") } + let(:another_client) { Elasticsearch::Client.new(host: "https://another-elasticsearch-host.example") } + + before do + Caoutsearch.configure do |config| + config.client = client + config.settings = { + number_of_shards: 5, + number_of_replicas: 2 + } + + config.instrument + + config.index do |index| + index.client = another_client + index.instrument :truncated + end + end + end + + after do + Caoutsearch.reset_config + end + + it "sets the default client" do + expect(Caoutsearch.config.client).to be(client) + end + + it "sets another client for indexes" do + expect(Caoutsearch.config.index.client).to be(another_client) + end + + it "sets the default client for searches" do + expect(Caoutsearch.config.search.client).to be(client) + end + + it "set indexes instrumentation" do + expect(Caoutsearch.config.index.instrumentation_format).to eq("truncated") + end + + it "set searches instrumentation" do + expect(Caoutsearch.config.search.instrumentation_format).to eq("full") + end + end + + context "with a custom search configuration" do + let(:client) { Elasticsearch::Client.new(host: "https://my-elasticsearch-host.example") } + let(:another_client) { Elasticsearch::Client.new(host: "https://another-elasticsearch-host.example") } + + before do + Caoutsearch.configure do |config| + config.client = client + config.settings = { + number_of_shards: 5, + number_of_replicas: 2 + } + + config.instrument + + config.search do |search| + search.client = another_client + search.instrument :amazing_print + end + end + end + + after do + Caoutsearch.reset_config + end + + it "sets the default client" do + expect(Caoutsearch.config.client).to be(client) + end + + it "sets another client for searches" do + expect(Caoutsearch.config.search.client).to be(another_client) + end + + it "sets the default client for indexes" do + expect(Caoutsearch.config.index.client).to be(client) + end + + it "set indexes instrumentation" do + expect(Caoutsearch.config.index.instrumentation_format).to eq("full") + end + + it "set searches instrumentation" do + expect(Caoutsearch.config.search.instrumentation_format).to eq("amazing_print") + end + end +end