├── CODEOWNERS ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug-report.md ├── SUPPORT.md └── workflows │ ├── rubocop.yml │ ├── sync_configs.yml │ ├── ci.yml │ └── docsite.yml ├── .rspec ├── lib ├── rom-elasticsearch.rb └── rom │ ├── elasticsearch │ ├── version.rb │ ├── errors.rb │ ├── index_name.rb │ ├── relation │ │ └── loaded.rb │ ├── schema.rb │ ├── types.rb │ ├── scroll_methods.rb │ ├── commands.rb │ ├── attribute.rb │ ├── query_methods.rb │ ├── plugins │ │ └── relation │ │ │ └── query_dsl.rb │ ├── gateway.rb │ ├── dataset.rb │ └── relation.rb │ └── elasticsearch.rb ├── project.yml ├── .yardopts ├── Rakefile ├── docker-compose.yml ├── spec ├── support │ ├── warnings.rb │ ├── coverage.rb │ └── rspec_options.rb ├── unit │ └── rom │ │ └── elasticsearch │ │ ├── gateway_spec.rb │ │ ├── dataset │ │ ├── body_spec.rb │ │ ├── put_spec.rb │ │ ├── scroll_spec.rb │ │ ├── params_spec.rb │ │ ├── delete_spec.rb │ │ ├── query_string_spec.rb │ │ └── search_spec.rb │ │ ├── relation │ │ ├── map_spec.rb │ │ ├── pluck_spec.rb │ │ ├── query_spec.rb │ │ ├── query_string_spec.rb │ │ ├── search_spec.rb │ │ ├── page_spec.rb │ │ ├── call_spec.rb │ │ ├── from_spec.rb │ │ ├── size_spec.rb │ │ ├── count_spec.rb │ │ ├── get_spec.rb │ │ ├── to_a_spec.rb │ │ ├── delete_spec.rb │ │ ├── scroll_spec.rb │ │ ├── order_spec.rb │ │ ├── dataset_spec.rb │ │ └── create_index_spec.rb │ │ └── plugins │ │ └── relation │ │ └── query_dsl_spec.rb ├── shared │ ├── unit │ │ ├── user_fixtures.rb │ │ └── users.rb │ └── setup.rb ├── spec_helper.rb └── integration │ └── rom │ └── elasticsearch │ └── relation │ ├── schema_spec.rb │ ├── command_spec.rb │ └── multi_index_spec.rb ├── .gitignore ├── Gemfile ├── .codeclimate.yml ├── Gemfile.devtools ├── .action_hero.yml ├── .devtools └── templates │ ├── release.erb │ └── changelog.erb ├── LICENSE ├── rom-elasticsearch.gemspec ├── README.md ├── changelog.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CHANGELOG.md └── .rubocop.yml /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @solnic 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | --warnings 5 | -------------------------------------------------------------------------------- /lib/rom-elasticsearch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch" 4 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: rom-elasticsearch 2 | custom_ci: true 3 | codacy_id: 9882fd022f904b48b5359b799b59ae88 4 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Elasticsearch 5 | VERSION = "0.5.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin junk 2 | --query '@api.text != "private"' 3 | --embed-mixins 4 | -r README.md 5 | --markup-provider=redcarpet 6 | --markup=markdown 7 | --files CHANGELOG.md 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discourse.rom-rb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | elasticsearch: 5 | image: docker.elastic.co/elasticsearch/elasticsearch:7.13.1 6 | ports: 7 | - "9200:9200" 8 | environment: 9 | discovery.type: "single-node" 10 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the rom-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.rom-rb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: 4 | -------------------------------------------------------------------------------- /spec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | .rubocop.yml 24 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/core" 4 | 5 | require "rom/elasticsearch/version" 6 | require "rom/elasticsearch/gateway" 7 | require "rom/elasticsearch/relation" 8 | require "rom/elasticsearch/commands" 9 | 10 | ROM.register_adapter(:elasticsearch, ROM::Elasticsearch) 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in rom-elasticsearch.gemspec 6 | gemspec 7 | 8 | eval_gemfile "Gemfile.devtools" 9 | 10 | gem "byebug", platform: :mri 11 | gem "elasticsearch-dsl" 12 | 13 | group :tools do 14 | gem "kramdown" # for yard 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/gateway_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/lint/spec" 4 | 5 | RSpec.describe ROM::Elasticsearch::Gateway do 6 | let(:uri) { "http://localhost:9200/rom-test" } 7 | 8 | it_behaves_like "a rom gateway" do 9 | let(:identifier) { :elasticsearch } 10 | let(:gateway) { ROM::Elasticsearch::Gateway } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools 4 | 5 | if ENV["COVERAGE"] == "true" 6 | require "simplecov" 7 | require "simplecov-cobertura" 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 10 | 11 | SimpleCov.start do 12 | add_filter "/spec/" 13 | enable_coverage :branch 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | prepare: 4 | fetch: 5 | - url: "https://raw.githubusercontent.com/rom-rb/devtools/master/.rubocop.yml" 6 | path: ".rubocop.yml" 7 | 8 | exclude_patterns: 9 | - "benchmarks/" 10 | - "examples/" 11 | - "spec/" 12 | - "*/spec/" 13 | - "*/examples/" 14 | - "*/benchmarks/" 15 | - "!*.rb" 16 | 17 | plugins: 18 | rubocop: 19 | enabled: true 20 | -------------------------------------------------------------------------------- /spec/shared/unit/user_fixtures.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "user fixtures" do 4 | include_context "setup" 5 | 6 | before do 7 | dataset.put(username: "eve") 8 | dataset.put(username: "bob") 9 | dataset.put(username: "alice") 10 | 11 | dataset.refresh 12 | end 13 | 14 | after do 15 | gateway[:users].refresh.delete 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/shared/unit/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "users" do 4 | include_context "setup" 5 | 6 | before do 7 | conf.relation(:users) do 8 | schema(:users) do 9 | attribute :id, ROM::Types::Integer 10 | attribute :name, ROM::Types::String 11 | 12 | primary_key :id 13 | end 14 | end 15 | end 16 | 17 | after do 18 | gateway[:users].refresh.delete 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/body_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#body" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "user fixtures" 9 | 10 | it "returns a new dataset with updated body" do 11 | new_ds = dataset.body(id: 1) 12 | 13 | expect(new_ds.body).to eql(id: 1) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/put_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#put" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "setup" 9 | 10 | it "puts new data" do 11 | result = dataset.put(username: "eve") 12 | 13 | expect(result["_id"]).to_not be(nil) 14 | expect(result["result"]).to eql("created") 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/scroll_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#scroll" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "user fixtures" 9 | 10 | it "returns a new dataset with updated params" do 11 | new_ds = dataset.scroll("5m") 12 | 13 | expect(new_ds.params).to eql(scroll: "5m", index: :users) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/params_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#params" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "user fixtures" 9 | 10 | it "returns a new dataset with updated params" do 11 | new_ds = dataset.params(size: 100) 12 | 13 | expect(new_ds.params).to eql(size: 100, index: :users) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/delete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#delete" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "user fixtures" 9 | 10 | it "deletes data" do 11 | expect(dataset.to_a.size).to eql(3) 12 | 13 | dataset.refresh.delete 14 | 15 | dataset.refresh 16 | 17 | expect(dataset.to_a.size).to eql(0) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/query_string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#query_string" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "user fixtures" 9 | 10 | it "returns data matching query string" do 11 | expect(dataset.query_string("username:alice").to_a).to eql([{"username" => "alice"}]) 12 | expect(dataset.query_string("username:nisse").to_a).to eql([]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/rspec_options.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | # When no filter given, search and run focused tests 3 | config.filter_run_when_matching :focus 4 | 5 | # Disables rspec monkey patches (no reason for their existence tbh) 6 | config.disable_monkey_patching! 7 | 8 | # Run ruby in verbose mode 9 | config.warnings = true 10 | 11 | # Collect all failing expectations automatically, 12 | # without calling aggregate_failures everywhere 13 | config.define_derived_metadata do |meta| 14 | meta[:aggregate_failures] = true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 8 | 9 | group :test do 10 | gem "simplecov", require: false, platforms: :ruby 11 | gem "simplecov-cobertura", require: false, platforms: :ruby 12 | gem "rexml", require: false 13 | 14 | gem "warning" if RUBY_VERSION >= "2.4.0" 15 | end 16 | 17 | group :tools do 18 | # this is the same version that we use on codacy 19 | gem "rubocop", "1.26.1" 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/map_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#map" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "yields result tuples" do 18 | expect(relation.map { |t| t[:name] }).to match_array(%w[Jane John]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/shared/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "setup" do 4 | let(:uri) { "http://127.0.0.1:9200" } 5 | 6 | let(:conf) { ROM::Configuration.new(:elasticsearch, client: Elasticsearch::Client.new(url: uri)) } 7 | let(:container) { ROM.container(conf) } 8 | 9 | let(:gateway) { conf.gateways[:default] } 10 | let(:client) { gateway.client } 11 | 12 | let(:relations) { container[:relations] } 13 | let(:commands) { container[:commands] } 14 | 15 | after do 16 | client.indices.delete(index: :users) if gateway.index?(:users) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/pluck_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#pluck" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns an array with plucked values" do 18 | expect(relation.pluck(:name)).to match_array(%w[Jane John]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/query_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#query" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns data matching query options" do 18 | expect(relation.query(match: {name: "Jane"}).one).to eql(id: 1, name: "Jane") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Elasticsearch 5 | # @api private 6 | class Error < StandardError 7 | def initialize(wrapped_error) 8 | super(wrapped_error.message) 9 | @wrapped_error = wrapped_error 10 | end 11 | 12 | attr_reader :wrapped_error 13 | end 14 | 15 | # @api private 16 | class SearchError < Error 17 | attr_reader :query 18 | 19 | def initialize(wrapped_error, query) 20 | super(wrapped_error) 21 | @query = query 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/query_string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#query_string" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns data matching query string" do 18 | expect(relation.query_string("name:Jane").one).to eql(id: 1, name: "Jane") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/search_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#search" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns data matching search options" do 18 | expect(relation.search(query: {match: {name: "Jane"}}).one).to eql(id: 1, name: "Jane") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /.action_hero.yml: -------------------------------------------------------------------------------- 1 | # This is a config synced from rom-rb/template-gem repo 2 | 3 | sources: 4 | - repo: rom-rb/template-gem 5 | sync: 6 | - "!.github/workflows/ci.yml.erb" 7 | - ".action_hero.yml.erb" 8 | - ".devtools/templates/*.sync:${{dir}}/${{name}}" 9 | - ".github/**/*.*" 10 | - "spec/support/*" 11 | - "CODE_OF_CONDUCT.md" 12 | - "CONTRIBUTING.md" 13 | - "CODEOWNERS" 14 | - "LICENSE.erb" 15 | - "README.erb" 16 | - "Gemfile.devtools" 17 | - ".rspec" 18 | - ".rubocop.yml" 19 | - repo: action-hero/workflows 20 | sync: 21 | - ".github/workflows/*.yml" 22 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/page_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#page" do 6 | subject(:relation) { relations[:users].order(:id) } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns relation with page set" do 18 | result = relation.per_page(1).page(2).to_a 19 | 20 | expect(result).to match_array([{id: 2, name: "John"}]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/index_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/support/inflector" 4 | 5 | module ROM 6 | module Elasticsearch 7 | # @api private 8 | class IndexName 9 | # @api private 10 | attr_reader :name 11 | 12 | def self.[](name) 13 | if name.is_a?(self) 14 | name 15 | else 16 | new(name) 17 | end 18 | end 19 | 20 | # @api private 21 | def initialize(name) 22 | @name = name 23 | freeze 24 | end 25 | 26 | # @api private 27 | def to_sym 28 | name 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/dataset/search_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Dataset, "#search" do 4 | subject(:dataset) do 5 | ROM::Elasticsearch::Dataset.new(client, params: {index: :users}) 6 | end 7 | 8 | include_context "setup" 9 | 10 | before do 11 | dataset.put(username: "eve") 12 | dataset.put(username: "bob") 13 | dataset.put(username: "alice") 14 | 15 | dataset.refresh 16 | end 17 | 18 | it "returns data matching query options" do 19 | expect(dataset.search(query: {query_string: {query: "username:eve"}}).to_a) 20 | .to eql([{"username" => "eve"}]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/call_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#call" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns loaded relation" do 18 | result = relation.call 19 | 20 | expect(result).to match_array([{id: 1, name: "Jane"}, {id: 2, name: "John"}]) 21 | 22 | expect(result.total_hits).to be(2) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/from_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#from" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "John") 12 | relation.command(:create).(id: 2, name: "Jane") 13 | relation.command(:create).(id: 3, name: "Jade") 14 | 15 | relation.refresh 16 | end 17 | 18 | it "returns a relation sliced by offset" do 19 | expect(relation.from(1).to_a.size).to eql 2 20 | expect(relation.from(2).to_a.size).to eql 1 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/size_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#size" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "John") 12 | relation.command(:create).(id: 2, name: "Jane") 13 | relation.command(:create).(id: 3, name: "Jade") 14 | 15 | relation.refresh 16 | end 17 | 18 | it "returns a relation limited by the number of documents" do 19 | expect(relation.size(1).to_a.size).to eql 1 20 | expect(relation.size(2).to_a.size).to eql 2 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/count_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#count" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "John") 12 | relation.command(:create).(id: 2, name: "Jane") 13 | relation.command(:create).(id: 3, name: "Jade") 14 | 15 | relation.refresh 16 | end 17 | 18 | it "returns a relation limited by the number of documents" do 19 | expect(relation.count).to eql 3 20 | expect(relation.query(match: {name: "Jane"}).count).to eql 1 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/get_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#get" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns user tuple by its id" do 18 | expect(relation.get(1).one).to eql(id: 1, name: "Jane") 19 | end 20 | 21 | it "raises search error" do 22 | expect { relation.get(12_421).one }.to raise_error(ROM::Elasticsearch::SearchError) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "byebug" 5 | rescue LoadError 6 | end 7 | 8 | require "support/coverage" 9 | require "rom-elasticsearch" 10 | 11 | SPEC_ROOT = Pathname(__FILE__).dirname 12 | 13 | Dir[SPEC_ROOT.join("shared/**/*.rb")].sort.each { |f| require f } 14 | 15 | RSpec.configure do |config| 16 | config.disable_monkey_patching! 17 | 18 | # elasticsearch-dsl warnings are killing me - solnic 19 | config.warnings = false 20 | 21 | # rubocop:disable Lint/ConstantDefinitionInBlock 22 | config.before do 23 | module Test 24 | end 25 | end 26 | 27 | config.after do 28 | Object.send(:remove_const, :Test) 29 | end 30 | # rubocop:enable Lint/ConstantDefinitionInBlock 31 | end 32 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/relation/loaded.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Elasticsearch 5 | class Relation < ROM::Relation 6 | # Materialized index data 7 | # 8 | # @api public 9 | class Loaded < ROM::Relation::Loaded 10 | # Return total number of hits 11 | # 12 | # @return [Integer] 13 | # 14 | # @api public 15 | def total_hits 16 | response["hits"]["total"]["value"] 17 | end 18 | 19 | # Return raw response from the ES client 20 | # 21 | # @return [Hash] 22 | # 23 | # @api public 24 | def response 25 | source.dataset.options[:response] 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/integration/rom/elasticsearch/relation/schema_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Relation, ".schema" do 4 | subject(:relation) { relations[:users] } 5 | 6 | include_context "setup" 7 | 8 | before do 9 | conf.relation(:users) do 10 | schema(:users) do 11 | attribute :id, ROM::Elasticsearch::Types::ID 12 | attribute :name, ROM::Elasticsearch::Types.Text, read: ROM::Types.Constructor(Symbol, &:to_sym) 13 | end 14 | end 15 | end 16 | 17 | it "defines read/write types" do 18 | relation.create_index 19 | 20 | relation.command(:create).call(id: 1, name: "Jane") 21 | 22 | user = relation.get(1).one 23 | 24 | expect(user[:id]).to be(1) 25 | expect(user[:name]).to be(:Jane) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/types" 4 | require "rom/schema" 5 | 6 | module ROM 7 | module Elasticsearch 8 | # Elasticsearch relation schema 9 | # 10 | # @api public 11 | class Schema < ROM::Schema 12 | # Return a hash with mapping properties 13 | # 14 | # @api private 15 | def to_properties 16 | select(&:properties?).map { |attr| [attr.name, attr.properties] }.to_h 17 | end 18 | 19 | # Customized output hash constructor which symbolizes keys 20 | # and optionally applies custom read-type coercions 21 | # 22 | # @api private 23 | def to_output_hash 24 | Types::Hash 25 | .schema(map { |attr| [attr.key, attr.to_read_type] }.to_h) 26 | .with_key_transform(&:to_sym) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/types" 4 | 5 | module ROM 6 | module Elasticsearch 7 | # Elasticsearch types use by schema attributes 8 | # 9 | # @api public 10 | module Types 11 | include ROM::Types 12 | 13 | # Default integer primary key 14 | ID = Integer.meta(primary_key: true) 15 | 16 | # Define a keyword attribute type 17 | # 18 | # @return [Dry::Types::Type] 19 | # 20 | # @api public 21 | def self.Keyword(meta = {}) 22 | String.meta(type: "keyword", **meta) 23 | end 24 | 25 | # Define a keyword attribute type 26 | # 27 | # @return [Dry::Types::Type] 28 | # 29 | # @api public 30 | def self.Text(meta = {}) 31 | String.meta(type: "text", **meta) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/to_a_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#to_a" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "returns user tuples" do 18 | expect(relation).to match_array([{id: 1, name: "Jane"}, {id: 2, name: "John"}]) 19 | end 20 | 21 | it "returns user structs" do 22 | jane, john = relation.with(auto_struct: true).to_a.sort_by(&:name) 23 | 24 | expect(jane.id).to be(1) 25 | expect(jane.name).to eql("Jane") 26 | 27 | expect(john.id).to be(2) 28 | expect(john.name).to eql("John") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.devtools/templates/release.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if latest_release.summary %> 5 | <%= latest_release.summary %> 6 | 7 | <% end %> 8 | 9 | <% if latest_release.added? %> 10 | ### Added 11 | 12 | <% latest_release.added.each do |log| %> 13 | - <%= log %> 14 | <% end %> 15 | 16 | <% end %> 17 | <% if latest_release.fixed? %> 18 | ### Fixed 19 | 20 | <% latest_release.fixed.each do |log| %> 21 | - <%= log %> 22 | <% end %> 23 | 24 | <% end %> 25 | <% if latest_release.changed? %> 26 | ### Changed 27 | 28 | <% latest_release.changed.each do |log| %> 29 | - <%= log %> 30 | <% end %> 31 | <% end %> 32 | <% if previous_release %> 33 | <% ver_range = "v#{previous_release.version}...v#{latest_release.version}" %> 34 | 35 | [Compare <%=ver_range%>](https://github.com/rom-rb/<%= project.name %>/compare/<%=ver_range%>) 36 | <% end %> 37 | -------------------------------------------------------------------------------- /.devtools/templates/changelog.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% releases.each_with_index do |r, idx| %> 5 | ## <%= r.version %> <%= r.date %> 6 | 7 | <% if r.summary %> 8 | <%= r.summary %> 9 | 10 | <% end %> 11 | 12 | <% if r.added? %> 13 | ### Added 14 | 15 | <% r.added.each do |log| %> 16 | - <%= log %> 17 | <% end %> 18 | 19 | <% end %> 20 | <% if r.fixed? %> 21 | ### Fixed 22 | 23 | <% r.fixed.each do |log| %> 24 | - <%= log %> 25 | <% end %> 26 | 27 | <% end %> 28 | <% if r.changed? %> 29 | ### Changed 30 | 31 | <% r.changed.each do |log| %> 32 | - <%= log %> 33 | <% end %> 34 | <% end %> 35 | <% curr_ver = r.date ? "v#{r.version}" : 'master' %> 36 | <% prev_rel = releases[idx + 1] %> 37 | <% if prev_rel %> 38 | <% ver_range = "v#{prev_rel.version}...#{curr_ver}" %> 39 | 40 | [Compare <%=ver_range%>](https://github.com/rom-rb/<%= project.name %>/compare/<%=ver_range%>) 41 | <% end %> 42 | 43 | <% end %> 44 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/delete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#delete" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | 14 | relation.refresh 15 | end 16 | 17 | it "deletes all data" do 18 | relation.delete 19 | 20 | expect(relation.refresh.to_a).to be_empty 21 | end 22 | 23 | it "deletes all data from a relation restricted by id" do 24 | relation.get(2).delete 25 | 26 | expect(relation.refresh.to_a).to eql([{id: 1, name: "Jane"}]) 27 | end 28 | 29 | it "deletes all data from a relation restricted by a query" do 30 | relation.query(match: {name: "Jane"}).delete 31 | 32 | expect(relation.refresh.to_a).to eql([{id: 2, name: "John"}]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/scroll_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#scroll" do 6 | subject(:relation) { relations[:users].order(:id) } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "Jane") 12 | relation.command(:create).(id: 2, name: "John") 13 | relation.command(:create).(id: 3, name: "David") 14 | relation.command(:create).(id: 4, name: "Dorin") 15 | relation.command(:create).(id: 5, name: "Petru") 16 | 17 | relation.refresh 18 | end 19 | 20 | it "returns relation with page set" do 21 | result = relation.per_page(2).scroll("5s").to_a 22 | 23 | expect(result).to match_array( 24 | [ 25 | {id: 1, name: "Jane"}, 26 | {id: 2, name: "John"}, 27 | {id: 3, name: "David"}, 28 | {id: 4, name: "Dorin"}, 29 | {id: 5, name: "Petru"} 30 | ] 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/plugins/relation/query_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/plugins/relation/query_dsl" 4 | require "rom/elasticsearch/relation" 5 | 6 | RSpec.describe ROM::Elasticsearch::Relation, "#search" do 7 | subject(:relation) { relations[:users] } 8 | 9 | include_context "setup" 10 | 11 | before do 12 | conf.relation(:users) do 13 | schema do 14 | attribute :id, ROM::Types::Integer.meta(type: "integer") 15 | attribute :name, ROM::Types::Integer.meta(type: "text") 16 | end 17 | 18 | use :query_dsl 19 | end 20 | 21 | relation.command(:create).(id: 1, name: "Jane") 22 | relation.command(:create).(id: 2, name: "John") 23 | 24 | relation.refresh 25 | end 26 | 27 | it "builds a query using a block-based DSL" do 28 | result = relation.search do 29 | query do 30 | match name: "Jane" 31 | end 32 | end 33 | 34 | expect(result.to_a).to eql([{id: 1, name: "Jane"}]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/scroll_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Elasticsearch 5 | # Dataset's scoll methods 6 | # 7 | # @see Dataset 8 | # 9 | # @api public 10 | module ScrollMethods 11 | # Return dataset with :scroll set 12 | # 13 | # @param [String] ttl 14 | # 15 | # @return [Dataset] 16 | # 17 | # @api public 18 | def scroll(ttl) 19 | params(scroll: ttl) 20 | end 21 | 22 | # @api private 23 | def scroll_enumerator(client, response) 24 | Enumerator.new do |yielder| 25 | loop do 26 | hits = response.fetch("hits").fetch("hits") 27 | break if hits.empty? 28 | 29 | hits.each { |result| yielder.yield(result) } 30 | 31 | response = client.scroll( 32 | scroll_id: response.fetch("_scroll_id"), 33 | scroll: params[:scroll] 34 | ) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 rom-rb team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /rom-elasticsearch.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rom/elasticsearch/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rom-elasticsearch' 8 | spec.version = ROM::Elasticsearch::VERSION 9 | spec.authors = ['Hannes Nevalainen', 'Piotr Solnica'] 10 | spec.email = ['hannes.nevalainen@me.com', 'piotr.solnica+oss@gmail.com'] 11 | spec.summary = %q{ROM adapter for Elasticsearch} 12 | spec.description = %q{} 13 | spec.homepage = 'https://rom-rb.org' 14 | spec.license = 'MIT' 15 | 16 | spec.files = Dir['CHANGELOG.md', 'LICENSE.txt', 'README.md', 'lib/**/*'] 17 | spec.executables = [] 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_runtime_dependency 'rom-core', '~> 5.2', '>= 5.2.5' 22 | spec.add_runtime_dependency 'elasticsearch', '~> 7.0' 23 | 24 | spec.add_development_dependency 'bundler' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'rspec' 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#order" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "users" 9 | 10 | before do 11 | relation.command(:create).(id: 1, name: "John") 12 | relation.command(:create).(id: 2, name: "Jane") 13 | relation.command(:create).(id: 3, name: "Jade") 14 | relation.command(:create).(id: 4, name: "Joe") 15 | 16 | relation.refresh 17 | end 18 | 19 | it "with ascending direction" do 20 | result = relation.order(:id).to_a 21 | 22 | expect(result) 23 | .to eql([ 24 | {id: 1, name: "John"}, 25 | {id: 2, name: "Jane"}, 26 | {id: 3, name: "Jade"}, 27 | {id: 4, name: "Joe"} 28 | ]) 29 | end 30 | 31 | it "with descending direction" do 32 | result = relation.order(relation[:id].desc).to_a 33 | 34 | expect(result) 35 | .to eql([ 36 | {id: 4, name: "Joe"}, 37 | {id: 3, name: "Jade"}, 38 | {id: 2, name: "Jane"}, 39 | {id: 1, name: "John"} 40 | ]) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: "RuboCop" 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "master" 7 | paths: 8 | - "**/*.rb" 9 | - "**/*.rake" 10 | - "Rakefile" 11 | - "Gemfile*" 12 | - ".rubocop.yml" 13 | pull_request: 14 | branches: 15 | - "main" 16 | - "master" 17 | types: 18 | - "opened" 19 | - "synchronize" 20 | workflow_dispatch: 21 | jobs: 22 | run: 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.type }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | type: ["Style", "Layout", "Naming", "Lint", "Metrics", "Security"] 29 | steps: 30 | - name: Clone 31 | uses: actions/checkout@v2 32 | - name: Get git diff 33 | id: get_diff 34 | uses: technote-space/get-diff-action@v4 35 | with: 36 | PATTERNS: | 37 | **/*.rb 38 | **/*.rake 39 | Gemfile 40 | Rakefile 41 | - name: Check ${{ matrix.type }} 42 | uses: action-hero/actions/rubocop@main 43 | if: ${{ env.GIT_DIFF != '' }} 44 | with: 45 | diff: ${{ env.GIT_DIFF }} 46 | type: ${{ matrix.type }} 47 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/commands" 4 | 5 | module ROM 6 | module Elasticsearch 7 | # ElasticSearch relation commands 8 | # 9 | # @api public 10 | class Commands 11 | # Create command 12 | # 13 | # @api public 14 | class Create < ROM::Commands::Create 15 | # @api private 16 | def execute(attributes) 17 | tuple = input[attributes] 18 | 19 | result = 20 | if _id 21 | dataset.params(id: tuple.fetch(_id)).put(tuple) 22 | else 23 | dataset.put(tuple) 24 | end 25 | [relation.get(result["_id"]).one] 26 | end 27 | 28 | private 29 | 30 | # @api private 31 | def dataset 32 | relation.dataset 33 | end 34 | 35 | def _id 36 | relation.schema.primary_key_name 37 | end 38 | end 39 | 40 | # Delete command 41 | # 42 | # @api public 43 | class Delete < ROM::Commands::Delete 44 | # @api private 45 | def execute 46 | relation.dataset.delete 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [gem]: https://rubygems.org/gems/rom-elasticsearch 2 | [actions]: https://github.com/rom-rb/rom-elasticsearch/actions 3 | [codacy]: https://www.codacy.com/gh/rom-rb/rom-elasticsearch 4 | [chat]: https://rom-rb.zulipchat.com 5 | [inchpages]: http://inch-ci.org/github/rom-rb/rom-elasticsearch 6 | 7 | # rom-elasticsearch [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-%23346b7a.svg)][chat] 8 | 9 | [![Gem Version](https://badge.fury.io/rb/rom-elasticsearch.svg)][gem] 10 | [![CI Status](https://github.com/rom-rb/rom-elasticsearch/workflows/ci/badge.svg)][actions] 11 | [![RuboCop](https://github.com/rom-rb/rom-elasticsearch/actions/workflows/rubocop.yml/badge.svg)](https://github.com/rom-rb/rom-elasticsearch/actions/workflows/rubocop.yml) 12 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9882fd022f904b48b5359b799b59ae88)][codacy] 13 | [![Inline docs](http://inch-ci.org/github/rom-rb/rom-elasticsearch.svg?branch=main)][inchpages] 14 | 15 | ## Links 16 | 17 | * [User documentation](http://rom-rb.org/learn/elasticsearch) 18 | * [API documentation](http://rubydoc.info/gems/rom-elasticsearch) 19 | 20 | ## Supported Ruby versions 21 | 22 | This library officially supports the following Ruby versions: 23 | 24 | * MRI >= `2.5` 25 | * jruby >= `9.2` 26 | 27 | ## License 28 | 29 | See `LICENSE` file. 30 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: 0.5.0 3 | summary: 4 | date: '2021-06-05' 5 | fixed: 6 | added: 7 | changed: 8 | - 'Upgrade to elasticsearch ~> 7.13 (via #25) (@demsullivan + @solnic)' 9 | - version: 0.4.0 10 | date: '2019-04-28' 11 | added: 12 | - 'New relation methods: `#count`, `#from` and `#size` (v-kolesnikov)' 13 | - Scroll API (hpetru) 14 | changed: 15 | - Updated to work with `rom ~> 5.0` (v-kolesnikov) 16 | - version: 0.3.0 17 | date: '2018-03-12' 18 | added: 19 | - 'Support for multi-index relations via `schema(multi: true)` (solnic)' 20 | - Datasets can be configured to include `_metadata` in resulting tuples (solnic) 21 | - version: 0.2.1 22 | date: '2018-02-06' 23 | fixed: 24 | - Using `read` types in schemas no longer breaks indexing (solnic) 25 | - version: 0.2.0 26 | date: '2018-01-23' 27 | added: 28 | - "`Relation#order` which sets `:sort` (solnic)" 29 | - "`Relation#page` which sets `:from` offset (solnic)" 30 | - "`Relation#per_page` which sets `:size` (solnic)" 31 | - "`Relation#call` returns custom `ROM::Elasticsearch::Relation::Loaded` object, 32 | which provides access to `#total_hits` and raw client response (solnic)" 33 | - version: 0.1.1 34 | date: '2017-11-18' 35 | changed: 36 | - Connection URI is passed directly to the ES client now (solnic) 37 | - version: 0.1.0 38 | date: '2017-11-17' 39 | summary: First public release 40 | -------------------------------------------------------------------------------- /spec/integration/rom/elasticsearch/relation/command_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Relation, "#command" do 4 | subject(:relation) { relations[:users] } 5 | 6 | include_context "setup" 7 | 8 | before do 9 | conf.relation(:users) do 10 | schema(:users) do 11 | attribute :id, ROM::Elasticsearch::Types::ID 12 | attribute :name, ROM::Types::String 13 | end 14 | end 15 | end 16 | 17 | describe ":create" do 18 | it "returns a create command" do 19 | command = relation.command(:create, result: :one) 20 | 21 | expect(command.call(id: 1, name: "Jane")).to eql(id: 1, name: "Jane") 22 | end 23 | 24 | it "applies input function" do 25 | command = relation.command(:create, result: :one) 26 | 27 | input = double(:user, to_hash: {id: 1, name: "Jane"}) 28 | 29 | expect(command.call(input)).to eql(id: 1, name: "Jane") 30 | end 31 | end 32 | 33 | describe ":delete" do 34 | before do 35 | relation.command(:create).call(id: 1, name: "Jane") 36 | relation.command(:create).call(id: 2, name: "John") 37 | 38 | relation.refresh 39 | end 40 | 41 | it "deletes matching data" do 42 | relation.get(2).command(:delete).call 43 | 44 | relation.refresh 45 | 46 | expect(relation.to_a).to eql([{id: 1, name: "Jane"}]) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.4.0, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct) 14 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/attribute" 4 | 5 | module ROM 6 | module Elasticsearch 7 | # ES-specific attribute types for schemas 8 | # 9 | # @api public 10 | class Attribute < ROM::Attribute 11 | INTERNAL_META_KEYS = %i[name source primary_key read].freeze 12 | DEFAULT_SORT_DIRECTION = "asc" 13 | 14 | # Return ES mapping properties 15 | # 16 | # @return [Hash] 17 | # 18 | # @api public 19 | memoize def properties 20 | type.meta.reject { |k, _| INTERNAL_META_KEYS.include?(k) } 21 | end 22 | 23 | # Return if an attribute has any ES mappings 24 | # 25 | # @return [Bool] 26 | # 27 | # @api public 28 | def properties? 29 | properties.size.positive? 30 | end 31 | 32 | # Return attribute with direction set to ascending 33 | # 34 | # @return [Attribute] 35 | # 36 | # @api public 37 | def asc 38 | meta(direction: "asc") 39 | end 40 | 41 | # Return attribute with direction set to descending 42 | # 43 | # @return [Attribute] 44 | # 45 | # @api public 46 | def desc 47 | meta(direction: "desc") 48 | end 49 | 50 | # @api private 51 | memoize def to_sort_expr 52 | "#{name}:#{direction}" 53 | end 54 | 55 | # @api private 56 | memoize def direction 57 | meta[:direction] || DEFAULT_SORT_DIRECTION 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue Guidelines 2 | 3 | ## Reporting bugs 4 | 5 | If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. 6 | 7 | ## Reporting feature requests 8 | 9 | Report a feature request **only after discussing it first on [discourse.rom-rb.org](https://discourse.rom-rb.org)** where it was accepted. Please provide a concise description of the feature. 10 | 11 | ## Reporting questions, support requests, ideas, concerns etc. 12 | 13 | **PLEASE DON'T** - use [discourse.rom-rb.org](https://discourse.rom-rb.org) instead. 14 | 15 | # Pull Request Guidelines 16 | 17 | A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. 18 | 19 | Other requirements: 20 | 21 | 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 22 | 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 23 | 3) Add API documentation if it's a new feature 24 | 4) Update API documentation if it changes an existing feature 25 | 5) Bonus points for sending a PR which updates user documentation in the `docsite` directory 26 | 27 | # Asking for help 28 | 29 | If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.rom-rb.org](https://discourse.rom-rb.org). 30 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/dataset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#dataset" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "setup" 9 | 10 | context "with default index settings" do 11 | before do 12 | conf.relation(:users) do 13 | schema do 14 | attribute :id, ROM::Types::Integer 15 | attribute :name, ROM::Types::String 16 | end 17 | end 18 | end 19 | 20 | it "sets up correct index name" do 21 | expect(relation.dataset.index).to eql(:users) 22 | end 23 | end 24 | 25 | context "overridding default dataset object" do 26 | let(:users) { relations[:users] } 27 | 28 | before do 29 | conf.relation(:users) do 30 | dataset do 31 | with(include_metadata: true) 32 | end 33 | 34 | schema do 35 | attribute :id, ROM::Types::Integer 36 | attribute :name, ROM::Types::String 37 | attribute :_metadata, ROM::Types::Hash, 38 | read: ROM::Types::Hash.schema(_index: ROM::Types::String) 39 | .with_key_transform(&:to_sym) 40 | end 41 | end 42 | 43 | users.create_index 44 | users.command(:create).call(id: 1, name: "Jane") 45 | users.refresh 46 | end 47 | 48 | it "uses customized dataset" do 49 | expect(relation.to_a).to eql([{id: 1, name: "Jane", _metadata: {_index: "users"}}]) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/query_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/errors" 4 | 5 | module ROM 6 | module Elasticsearch 7 | # Dataset's query methods 8 | # 9 | # @see Dataset 10 | # 11 | # @api public 12 | module QueryMethods 13 | # Return a new dataset configured to search by :id 14 | # 15 | # @param [Integer] id 16 | # 17 | # @return [Dataset] 18 | # 19 | # @see Relation#get 20 | # 21 | # @api public 22 | def get(id) 23 | params(id: id) 24 | end 25 | 26 | # Return a new dataset configured to search via new body options 27 | # 28 | # @param [Hash] options Body options 29 | # 30 | # @return [Dataset] 31 | # 32 | # @see Relation#search 33 | # 34 | # @api public 35 | def search(options) 36 | body(options) 37 | end 38 | 39 | # Return a new dataset configured to search via :query_string body option 40 | # 41 | # @param [String] expression A string query 42 | # 43 | # @return [Dataset] 44 | # 45 | # @see Relation#query_string 46 | # 47 | # @api public 48 | def query_string(expression) 49 | query(query_string: {query: expression}) 50 | end 51 | 52 | # Return a new dataset configured to search via :query body option 53 | # 54 | # @param [Hash] query A query hash 55 | # 56 | # @return [Dataset] 57 | # 58 | # @see Relation#query 59 | # 60 | # @api public 61 | def query(query) 62 | body(query: query) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /.github/workflows/sync_configs.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-rb/template-gem repo 2 | 3 | name: Changelog 4 | 5 | on: 6 | push: 7 | paths: 8 | - "changelog.yml" 9 | branches: 10 | - "main" 11 | pull_request: 12 | branches: 13 | - "main" 14 | types: [closed] 15 | 16 | jobs: 17 | sync: 18 | runs-on: ubuntu-latest 19 | if: github.event.pull_request.merged == true 20 | name: Update 21 | env: 22 | GITHUB_LOGIN: rom-bot 23 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 24 | steps: 25 | - name: Checkout ${{github.repository}} 26 | uses: actions/checkout@v1 27 | - name: Checkout devtools 28 | uses: actions/checkout@v2 29 | with: 30 | repository: rom-rb/devtools 31 | path: tmp/devtools 32 | - name: Setup git user 33 | run: | 34 | git config --local user.email "rom-bot@rom-rb.org" 35 | git config --local user.name "rom-bot" 36 | - name: Set up Ruby 37 | uses: actions/setup-ruby@v1 38 | with: 39 | ruby-version: "2.6" 40 | - name: Install dependencies 41 | run: gem install ossy --no-document 42 | - name: Update changelog.yml from commit 43 | run: tmp/devtools/bin/update-changelog-from-commit $GITHUB_SHA 44 | - name: Compile CHANGELOG.md 45 | run: tmp/devtools/bin/compile-changelog 46 | - name: Commit 47 | run: | 48 | git add -A 49 | git commit -m "Update CHANGELOG.md" || echo "nothing to commit" 50 | - name: Push changes 51 | run: | 52 | git pull --rebase origin main 53 | git push https://rom-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:main 54 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/plugins/relation/query_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/global" 4 | require "elasticsearch/dsl" 5 | 6 | module ROM 7 | module Elasticsearch 8 | module Plugins 9 | module Relation 10 | # Relation plugin which adds query DSL from elasticsearch-dsl gem 11 | # 12 | # @api public 13 | module QueryDSL 14 | # @api private 15 | def self.included(klass) 16 | super 17 | klass.include(InstanceMethods) 18 | klass.option :query_builder, default: -> { Builder.new(self) } 19 | end 20 | 21 | # @api public 22 | module InstanceMethods 23 | # Restrict a relation via query DSL 24 | # 25 | # @see Relation#search 26 | # @see https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl 27 | # 28 | # @return [Relation] 29 | # 30 | # @api public 31 | def search(options = EMPTY_HASH, &block) 32 | if block 33 | super(query_builder.search(&block).to_hash) 34 | else 35 | super 36 | end 37 | end 38 | end 39 | 40 | class Builder 41 | include ::Elasticsearch::DSL 42 | 43 | attr_reader :relation 44 | 45 | def initialize(relation) 46 | @relation = relation 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | plugins do 55 | adapter :elasticsearch do 56 | register :query_dsl, ROM::Elasticsearch::Plugins::Relation::QueryDSL, type: :relation 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-rb/template-gem repo 2 | name: ci 3 | 4 | on: 5 | push: 6 | paths: 7 | - ".github/workflows/ci.yml" 8 | - "lib/**" 9 | - "*.gemspec" 10 | - "spec/**" 11 | - "Rakefile" 12 | - "Gemfile" 13 | - "Gemfile.devtools" 14 | - ".rubocop.yml" 15 | - "project.yml" 16 | pull_request: 17 | branches: 18 | - main 19 | create: 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | name: Tests 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | ruby: 29 | - "3.1" 30 | - "3.0" 31 | - "2.7" 32 | include: 33 | - ruby: "3.1" 34 | coverage: "true" 35 | env: 36 | COVERAGE: ${{matrix.coverage}} 37 | COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v1 41 | - name: Install package dependencies 42 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 43 | - name: Set up Ruby 44 | uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | - name: Install latest bundler 48 | run: | 49 | gem install bundler --no-document 50 | bundle config set without 'tools benchmarks docs' 51 | - name: Bundle install 52 | run: bundle install --jobs 4 --retry 3 53 | - name: Run all tests 54 | run: bundle exec rake 55 | - name: Run codacy-coverage-reporter 56 | uses: codacy/codacy-coverage-reporter-action@main 57 | if: env.COVERAGE == 'true' && env.COVERAGE_TOKEN != '' 58 | with: 59 | project-token: ${{secrets.CODACY_PROJECT_TOKEN}} 60 | coverage-reports: coverage/coverage.xml 61 | -------------------------------------------------------------------------------- /spec/integration/rom/elasticsearch/relation/multi_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Elasticsearch::Relation, ".schema" do 4 | subject(:relation) { relations[:search] } 5 | 6 | include_context "setup" 7 | 8 | let(:posts) { relations[:posts] } 9 | let(:pages) { relations[:pages] } 10 | 11 | before do 12 | conf.relation(:posts) do 13 | schema(:posts) do 14 | attribute :id, ROM::Elasticsearch::Types::ID 15 | attribute :title, ROM::Elasticsearch::Types.Text 16 | end 17 | end 18 | 19 | conf.relation(:pages) do 20 | schema(:pages) do 21 | attribute :id, ROM::Elasticsearch::Types::ID 22 | attribute :title, ROM::Elasticsearch::Types.Text 23 | end 24 | end 25 | 26 | conf.relation(:search) do 27 | multi_index_types %i[post page] 28 | 29 | schema(multi: true) do 30 | attribute :id, ROM::Elasticsearch::Types::ID 31 | attribute :title, ROM::Elasticsearch::Types.Text 32 | end 33 | end 34 | 35 | posts.create_index 36 | pages.create_index 37 | end 38 | 39 | after do 40 | posts.delete_index 41 | pages.delete_index 42 | end 43 | 44 | it "search through specified indices" do 45 | posts.command(:create).call(id: 1, title: "Post 1") 46 | posts.command(:create).call(id: 2, title: "Post 2") 47 | 48 | pages.command(:create).call(id: 1, title: "Page 1") 49 | pages.command(:create).call(id: 2, title: "Page 2") 50 | 51 | posts.refresh 52 | pages.refresh 53 | 54 | result = relation.query(match: {title: "Post"}) 55 | 56 | expect(result.to_a).to eql([{id: 1, title: "Post 1"}, {id: 2, title: "Post 2"}]) 57 | 58 | result = relation.query(match: {title: "1"}) 59 | 60 | expect(result.to_a).to eql([{id: 1, title: "Page 1"}, {id: 1, title: "Post 1"}]) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /.github/workflows/docsite.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-rb/template-gem repo 2 | 3 | name: docsite 4 | 5 | on: 6 | push: 7 | paths: 8 | - docsite/** 9 | - .github/workflows/docsite.yml 10 | branches: 11 | - main 12 | - release-** 13 | tags: 14 | 15 | jobs: 16 | update-docs: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - run: | 23 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 24 | - name: Set up Ruby 25 | uses: actions/setup-ruby@v1 26 | with: 27 | ruby-version: "2.6.x" 28 | - name: Set up git user 29 | run: | 30 | git config --local user.email "rom-bot@rom-rb.org" 31 | git config --local user.name "rom-bot" 32 | - name: Install dependencies 33 | run: gem install ossy --no-document 34 | - name: Update release branches 35 | run: | 36 | branches=`git log --format=%B -n 1 $GITHUB_SHA | grep "docsite:release-" || echo "nothing"` 37 | 38 | if [[ ! $branches -eq "nothing" ]]; then 39 | for b in $branches 40 | do 41 | name=`echo $b | ruby -e 'puts gets[/:(.+)/, 1].gsub(/\s+/, "")'` 42 | 43 | echo "merging $GITHUB_SHA to $name" 44 | 45 | git checkout -b $name --track origin/$name 46 | 47 | echo `git log -n 1` 48 | 49 | git cherry-pick $GITHUB_SHA -m 1 50 | done 51 | 52 | git push --all "https://rom-bot:${{secrets.GH_PAT}}@github.com/$GITHUB_REPOSITORY.git" 53 | 54 | git checkout main 55 | else 56 | echo "no need to update branches" 57 | fi 58 | - name: Trigger rom-rb.org deploy 59 | env: 60 | GITHUB_LOGIN: rom-bot 61 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 62 | run: ossy github workflow rom-rb/rom-rb.org ci 63 | 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 0.5.0 2021-06-05 4 | 5 | 6 | ### Changed 7 | 8 | - Upgrade to elasticsearch ~> 7.13 (via #25) (@demsullivan + @solnic) 9 | 10 | [Compare v0.4.0...v0.5.0](https://github.com/rom-rb/rom-elasticsearch/compare/v0.4.0...v0.5.0) 11 | 12 | ## 0.4.0 2019-04-28 13 | 14 | 15 | ### Added 16 | 17 | - New relation methods: `#count`, `#from` and `#size` (v-kolesnikov) 18 | - Scroll API (hpetru) 19 | 20 | ### Changed 21 | 22 | - Updated to work with `rom ~> 5.0` (v-kolesnikov) 23 | 24 | [Compare v0.3.0...v0.4.0](https://github.com/rom-rb/rom-elasticsearch/compare/v0.3.0...v0.4.0) 25 | 26 | ## 0.3.0 2018-03-12 27 | 28 | 29 | ### Added 30 | 31 | - Support for multi-index relations via `schema(multi: true)` (solnic) 32 | - Datasets can be configured to include `_metadata` in resulting tuples (solnic) 33 | 34 | 35 | [Compare v0.2.1...v0.3.0](https://github.com/rom-rb/rom-elasticsearch/compare/v0.2.1...v0.3.0) 36 | 37 | ## 0.2.1 2018-02-06 38 | 39 | 40 | ### Fixed 41 | 42 | - Using `read` types in schemas no longer breaks indexing (solnic) 43 | 44 | 45 | [Compare v0.2.0...v0.2.1](https://github.com/rom-rb/rom-elasticsearch/compare/v0.2.0...v0.2.1) 46 | 47 | ## 0.2.0 2018-01-23 48 | 49 | 50 | ### Added 51 | 52 | - `Relation#order` which sets `:sort` (solnic) 53 | - `Relation#page` which sets `:from` offset (solnic) 54 | - `Relation#per_page` which sets `:size` (solnic) 55 | - `Relation#call` returns custom `ROM::Elasticsearch::Relation::Loaded` object, which provides access to `#total_hits` and raw client response (solnic) 56 | 57 | 58 | [Compare v0.1.1...v0.2.0](https://github.com/rom-rb/rom-elasticsearch/compare/v0.1.1...v0.2.0) 59 | 60 | ## 0.1.1 2017-11-18 61 | 62 | 63 | ### Changed 64 | 65 | - Connection URI is passed directly to the ES client now (solnic) 66 | 67 | [Compare v0.1.0...v0.1.1](https://github.com/rom-rb/rom-elasticsearch/compare/v0.1.0...v0.1.1) 68 | 69 | ## 0.1.0 2017-11-17 70 | 71 | First public release 72 | -------------------------------------------------------------------------------- /spec/unit/rom/elasticsearch/relation/create_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/elasticsearch/relation" 4 | 5 | RSpec.describe ROM::Elasticsearch::Relation, "#create_index" do 6 | subject(:relation) { relations[:users] } 7 | 8 | include_context "setup" 9 | 10 | context "when custom :index is configured" do 11 | after do 12 | relation.delete_index 13 | end 14 | 15 | context "with default settings" do 16 | before do 17 | conf.relation(:users) do 18 | schema do 19 | attribute :id, ROM::Elasticsearch::Types::ID 20 | attribute :name, ROM::Types::String 21 | end 22 | end 23 | end 24 | 25 | it "creates an index" do 26 | relation.create_index 27 | 28 | expect(gateway.index?(:users)).to be(true) 29 | end 30 | end 31 | 32 | context "with customized settings" do 33 | before do 34 | conf.relation(:users) do 35 | schema do 36 | attribute :id, ROM::Types::Integer 37 | attribute :name, ROM::Types::String 38 | end 39 | 40 | index_settings number_of_shards: 2 41 | end 42 | end 43 | 44 | it "creates an index" do 45 | relation.create_index 46 | 47 | expect(gateway.index?(:users)).to be(true) 48 | expect(relation.dataset.settings["number_of_shards"]).to eql("2") 49 | end 50 | end 51 | 52 | context "with customized attribute mappings" do 53 | before do 54 | conf.relation(:users) do 55 | schema do 56 | attribute :id, ROM::Elasticsearch::Types::ID 57 | attribute :name, ROM::Elasticsearch::Types.Keyword 58 | attribute :desc, ROM::Elasticsearch::Types.Text(analyzer: "snowball") 59 | end 60 | 61 | index_settings number_of_shards: 2 62 | end 63 | end 64 | 65 | it "creates an index" do 66 | relation.create_index 67 | 68 | expect(gateway.index?(:users)).to be(true) 69 | 70 | expect(relation.dataset.mappings) 71 | .to eql("properties" => { 72 | "name" => {"type" => "keyword"}, 73 | "desc" => {"type" => "text", "analyzer" => "snowball"} 74 | }) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/gateway.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "elasticsearch" 4 | require "uri" 5 | 6 | require "rom/gateway" 7 | require "rom/support/inflector" 8 | require "rom/elasticsearch/dataset" 9 | require "rom/elasticsearch/index_name" 10 | 11 | module ROM 12 | module Elasticsearch 13 | # Elasticsearch gateway 14 | # 15 | # @example basic configuration 16 | # conf = ROM::Configuration.new(:elasticsearch, 'http://localhost:9200') 17 | # 18 | # class Posts < ROM::Relation[:elasticsearch] 19 | # schema(:posts) do 20 | # attribute :id, Types::Int 21 | # attribute :title, Types::String 22 | # 23 | # primary_key :id 24 | # end 25 | # 26 | # def like(title) 27 | # query(prefix: { title: title }) 28 | # end 29 | # end 30 | # 31 | # conf.register_relation(Posts) 32 | # 33 | # rom = ROM.container(conf) 34 | # 35 | # posts = rom.relations[:posts] 36 | # 37 | # posts.command(:create).call(id: 1, title: 'Hello World') 38 | # 39 | # posts.like('Hello').first 40 | # 41 | # @example using an existing client 42 | # client = Elasticsearch::Client.new('http://localhost:9200') 43 | # conf = ROM::Configuration.new(:elasticsearch, client: client) 44 | # 45 | # @api public 46 | class Gateway < ROM::Gateway 47 | extend ROM::Initializer 48 | 49 | adapter :elasticsearch 50 | 51 | # @!attribute [r] url 52 | # @return [URI] Connection URL 53 | attr_reader :url 54 | 55 | # @!attribute [r] client 56 | # @return [::Elasticsearch::Client] configured ES client 57 | attr_reader :client 58 | 59 | param :uri, default: proc {} 60 | option :client, default: -> { ::Elasticsearch::Client.new(url: uri, log: log) } 61 | option :log, default: -> { false } 62 | 63 | # Return true if a dataset with the given :index exists 64 | # 65 | # @param [Symbol] index The name of the index 66 | # 67 | # @return [Boolean] 68 | # 69 | # @api public 70 | def dataset?(index) 71 | client.indices.exists?(index: index) 72 | end 73 | alias_method :index?, :dataset? 74 | 75 | # Get a dataset by its :index name 76 | # 77 | # @param [Symbol] index The name of the index 78 | # 79 | # @return [Dataset] 80 | # 81 | # @api public 82 | def dataset(index) 83 | idx_name = IndexName[index] 84 | Dataset.new(client, params: {index: idx_name.to_sym}) 85 | end 86 | alias_method :[], :dataset 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This is a config synced from rom-rb/template-gem repo 2 | AllCops: 3 | TargetRubyVersion: 2.7 4 | NewCops: disable 5 | SuggestExtensions: false 6 | Exclude: 7 | - spec/support/coverage.rb 8 | - spec/support/warnings.rb 9 | - spec/support/rspec_options.rb 10 | - Gemfile.devtools 11 | - "*.gemspec" 12 | 13 | Layout/ArgumentAlignment: 14 | Enabled: false 15 | EnforcedStyle: with_fixed_indentation 16 | 17 | Layout/SpaceAroundMethodCallOperator: 18 | Enabled: false 19 | 20 | Layout/SpaceInLambdaLiteral: 21 | Enabled: false 22 | 23 | Layout/MultilineMethodCallIndentation: 24 | Enabled: true 25 | EnforcedStyle: indented 26 | 27 | Layout/FirstArrayElementIndentation: 28 | EnforcedStyle: consistent 29 | 30 | Layout/SpaceInsideHashLiteralBraces: 31 | Enabled: true 32 | EnforcedStyle: no_space 33 | EnforcedStyleForEmptyBraces: no_space 34 | 35 | Layout/LineLength: 36 | Max: 120 37 | Exclude: 38 | - "spec/**/*_spec.rb" 39 | 40 | Lint/AmbiguousBlockAssociation: 41 | Enabled: true 42 | # because 'expect { foo }.to change { bar }' is fine 43 | Exclude: 44 | - "spec/**/*.rb" 45 | 46 | Lint/BooleanSymbol: 47 | Enabled: false 48 | 49 | Lint/MissingSuper: 50 | Enabled: false 51 | 52 | Lint/ConstantDefinitionInBlock: 53 | Enabled: false 54 | 55 | Lint/EmptyBlock: 56 | Exclude: 57 | - "spec/**/*.rb" 58 | 59 | Lint/EmptyClass: 60 | Exclude: 61 | - "spec/**/*.rb" 62 | 63 | Lint/RaiseException: 64 | Enabled: false 65 | 66 | Lint/StructNewOverride: 67 | Enabled: false 68 | 69 | Lint/SuppressedException: 70 | Exclude: 71 | - "spec/**/*.rb" 72 | 73 | Lint/UselessMethodDefinition: 74 | Exclude: 75 | - "lib/rom/struct.rb" 76 | 77 | Lint/UnderscorePrefixedVariableName: 78 | Enabled: false 79 | 80 | Lint/ToEnumArguments: 81 | Exclude: 82 | - "lib/rom/command.rb" 83 | 84 | Naming/MethodParameterName: 85 | Enabled: false 86 | 87 | Naming/AccessorMethodName: 88 | Enabled: false 89 | 90 | Naming/VariableNumber: 91 | Enabled: false 92 | 93 | Naming/PredicateName: 94 | Enabled: false 95 | 96 | Naming/FileName: 97 | Exclude: 98 | - "lib/*-*.rb" 99 | 100 | Naming/MethodName: 101 | Enabled: false 102 | 103 | Naming/MemoizedInstanceVariableName: 104 | Enabled: false 105 | 106 | Metrics/MethodLength: 107 | Enabled: false 108 | 109 | Metrics/ModuleLength: 110 | Enabled: false 111 | 112 | Metrics/ClassLength: 113 | Enabled: false 114 | 115 | Metrics/BlockLength: 116 | Enabled: false 117 | 118 | Metrics/AbcSize: 119 | Max: 36 120 | 121 | Metrics/CyclomaticComplexity: 122 | Enabled: true 123 | Max: 12 124 | 125 | Metrics/PerceivedComplexity: 126 | Max: 14 127 | 128 | Metrics/ParameterLists: 129 | Enabled: false 130 | 131 | Style/AccessorGrouping: 132 | Enabled: false 133 | 134 | Style/ExponentialNotation: 135 | Enabled: false 136 | 137 | Style/HashEachMethods: 138 | Enabled: false 139 | 140 | Style/HashTransformKeys: 141 | Enabled: false 142 | 143 | Style/HashTransformValues: 144 | Enabled: false 145 | 146 | Style/AccessModifierDeclarations: 147 | Enabled: false 148 | 149 | Style/Alias: 150 | Enabled: true 151 | EnforcedStyle: prefer_alias_method 152 | 153 | Style/AsciiComments: 154 | Enabled: false 155 | 156 | Style/BlockDelimiters: 157 | Enabled: false 158 | 159 | Style/ClassAndModuleChildren: 160 | Enabled: false 161 | 162 | Style/ConditionalAssignment: 163 | Enabled: false 164 | 165 | Style/DateTime: 166 | Enabled: false 167 | 168 | Style/Documentation: 169 | Enabled: false 170 | 171 | Style/EachWithObject: 172 | Enabled: false 173 | 174 | Style/FormatString: 175 | Enabled: false 176 | 177 | Style/GuardClause: 178 | Enabled: false 179 | 180 | Style/IfUnlessModifier: 181 | Enabled: false 182 | 183 | Style/Lambda: 184 | Enabled: false 185 | 186 | Style/LambdaCall: 187 | Enabled: false 188 | 189 | Style/ParallelAssignment: 190 | Enabled: false 191 | 192 | Style/StabbyLambdaParentheses: 193 | Enabled: false 194 | 195 | Style/StringLiterals: 196 | Enabled: true 197 | EnforcedStyle: double_quotes 198 | ConsistentQuotesInMultiline: false 199 | 200 | Style/StringLiteralsInInterpolation: 201 | Enabled: true 202 | EnforcedStyle: double_quotes 203 | 204 | Style/SymbolArray: 205 | Enabled: false 206 | 207 | Style/OptionalBooleanParameter: 208 | Enabled: false 209 | 210 | Style/MultilineBlockChain: 211 | Enabled: false 212 | 213 | Style/DocumentDynamicEvalDefinition: 214 | Enabled: false 215 | 216 | Style/TrailingUnderscoreVariable: 217 | Enabled: false 218 | 219 | Style/MultipleComparison: 220 | Enabled: false 221 | 222 | Style/StringConcatenation: 223 | Enabled: false 224 | 225 | Style/OpenStructUse: 226 | Enabled: false 227 | 228 | Style/MapToHash: 229 | Enabled: false 230 | 231 | Style/FormatStringToken: 232 | Enabled: false 233 | 234 | Style/StructInheritance: 235 | Enabled: false 236 | 237 | Style/PreferredHashMethods: 238 | Enabled: false 239 | 240 | Style/DoubleNegation: 241 | Enabled: false 242 | 243 | Style/MissingRespondToMissing: 244 | Enabled: false 245 | 246 | Style/CombinableLoops: 247 | Enabled: false 248 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/dataset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/initializer" 4 | 5 | require "rom/elasticsearch/query_methods" 6 | require "rom/elasticsearch/scroll_methods" 7 | require "rom/elasticsearch/errors" 8 | 9 | module ROM 10 | module Elasticsearch 11 | # Elasticsearch dataset 12 | # 13 | # Uses an elasticsearch client object provided by the gateway, holds basic 14 | # params with information about index name and type, and optional body for 15 | # additional queries. 16 | # 17 | # Dataset object also provide meta information about indices, like custom 18 | # settings and mappings. 19 | # 20 | # @api public 21 | class Dataset 22 | extend Initializer 23 | 24 | include QueryMethods 25 | include ScrollMethods 26 | 27 | # Sort values separator 28 | SORT_VALUES_SEPARATOR = "," 29 | 30 | # Default query options 31 | ALL = {query: {match_all: EMPTY_HASH}}.freeze 32 | 33 | # The source key in raw results 34 | SOURCE_KEY = "_source" 35 | 36 | # @!attribute [r] client 37 | # @return [::Elasticsearch::Client] configured client from the gateway 38 | param :client 39 | 40 | # @!attribute [r] params 41 | # @return [Hash] default params 42 | option :params, default: -> { EMPTY_HASH } 43 | 44 | # @!attribute [r] client 45 | # @return [Hash] default body 46 | option :body, default: -> { EMPTY_HASH } 47 | 48 | # @!attribute [r] include_metadata 49 | # @return [Bool] 50 | option :include_metadata, default: -> { false } 51 | 52 | # @!attribute [r] response 53 | # @return [Hash] memoized response from the client 54 | option :response, optional: true, reader: false 55 | 56 | # @!attribute [r] tuple_proc 57 | # @return [Proc] low-level tuple processing function used in #each 58 | attr_reader :tuple_proc 59 | 60 | # default tuple proc which extracts raw source data from response item 61 | TUPLE_PROC = -> t { t[SOURCE_KEY] } 62 | 63 | # tuple proc used when :include_metadata is enabled, resulting tuples 64 | # will include raw response hash under _metadata key 65 | TUPLE_PROC_WITH_METADATA = -> t { TUPLE_PROC[t].merge(_metadata: t) } 66 | 67 | # @api private 68 | def initialize(*args, **kwargs) 69 | super 70 | @tuple_proc = options[:include_metadata] ? TUPLE_PROC_WITH_METADATA : TUPLE_PROC 71 | end 72 | 73 | # Put new data under configured index 74 | # 75 | # @param [Hash] data 76 | # 77 | # @return [Hash] 78 | # 79 | # @api public 80 | def put(data) 81 | client.index(**params, body: data) 82 | end 83 | 84 | # Return index settings 85 | # 86 | # @return [Hash] 87 | # 88 | # @api public 89 | def settings 90 | client.indices.get_settings[index.to_s]["settings"]["index"] 91 | end 92 | 93 | # Return index mappings 94 | # 95 | # @return [Hash] 96 | # 97 | # @api public 98 | def mappings 99 | client.indices.get_mapping[index.to_s]["mappings"] 100 | end 101 | 102 | # Delete everything matching configured params and/or body 103 | # 104 | # If body is empty it *will delete everything** 105 | # 106 | # @return [Hash] raw response hash from the client 107 | # 108 | # @api public 109 | def delete 110 | if body.empty? && params[:id] 111 | client.delete(params) 112 | elsif body.empty? 113 | client.delete_by_query(params.merge(body: body.merge(ALL))) 114 | else 115 | client.delete_by_query(params.merge(body: body)) 116 | end 117 | end 118 | 119 | # Materialize the dataset 120 | # 121 | # @return [Array] 122 | # 123 | # @api public 124 | def to_a 125 | to_enum.to_a 126 | end 127 | 128 | # Materialize and iterate over results 129 | # 130 | # @yieldparam [Hash] 131 | # 132 | # @raise [SearchError] in case of the client raising an exception 133 | # 134 | # @api public 135 | def each 136 | return to_enum unless block_given? 137 | 138 | view.each { |result| yield(tuple_proc[result]) } 139 | rescue ::Elasticsearch::Transport::Transport::Error => e 140 | raise SearchError.new(e, options) 141 | end 142 | 143 | # Map dataset tuples 144 | # 145 | # @yieldparam [Hash] 146 | # 147 | # @return [Array] 148 | # 149 | # @api public 150 | def map(&block) 151 | to_a.map(&block) 152 | end 153 | 154 | # Return configured type from params 155 | # 156 | # @return [Symbol] 157 | # 158 | # @api public 159 | def type 160 | params[:type] 161 | end 162 | 163 | # Return configured index name 164 | # 165 | # @return [Symbol] 166 | # 167 | # @api public 168 | def index 169 | params[:index] 170 | end 171 | 172 | # Return a new dataset with new body 173 | # 174 | # @param [Hash] new New body data 175 | # 176 | # @return [Hash] 177 | # 178 | # @api public 179 | def body(new = nil) 180 | if new.nil? 181 | @body 182 | else 183 | with(body: body.merge(new)) 184 | end 185 | end 186 | 187 | # Return a new dataset with new params 188 | # 189 | # @param [Hash] new New params data 190 | # 191 | # @return [Hash] 192 | # 193 | # @api public 194 | def params(new = nil) 195 | if new.nil? 196 | @params 197 | else 198 | with(params: params.merge(new)) 199 | end 200 | end 201 | 202 | # Refresh index 203 | # 204 | # @return [Dataset] 205 | # 206 | # @api public 207 | def refresh 208 | client.indices.refresh(index: index) 209 | self 210 | end 211 | 212 | # Return dataset with :sort set 213 | # 214 | # @return [Dataset] 215 | # 216 | # @api public 217 | def sort(*fields) 218 | params(sort: fields.join(SORT_VALUES_SEPARATOR)) 219 | end 220 | 221 | # Return dataset with :from set 222 | # 223 | # @param [Integer] num 224 | # 225 | # @return [Dataset] 226 | # 227 | # @api public 228 | def from(num) 229 | params(from: num) 230 | end 231 | 232 | # Return dataset with :size set 233 | # 234 | # @param [Integer] num 235 | # 236 | # @return [Dataset] 237 | # 238 | # @api public 239 | def size(num) 240 | params(size: num) 241 | end 242 | 243 | # Create an index 244 | # 245 | # @param [Hash] opts ES options 246 | # 247 | # @api public 248 | # 249 | # @return [Hash] 250 | def create_index(opts = EMPTY_HASH) 251 | client.indices.create(params.merge(opts)) 252 | end 253 | 254 | # Delete an index 255 | # 256 | # @param [Hash] opts ES options 257 | # 258 | # @api public 259 | # 260 | # @return [Hash] 261 | def delete_index(opts = EMPTY_HASH) 262 | client.indices.delete(params.merge(opts)) 263 | end 264 | 265 | # Return a dataset with pre-set client response 266 | # 267 | # @return [Dataset] 268 | # 269 | # @api public 270 | def call 271 | with(response: response) 272 | end 273 | 274 | private 275 | 276 | # Return results of a query based on configured params and body 277 | # 278 | # @return [Array] 279 | # 280 | # @api private 281 | def view 282 | if params[:id] 283 | [client.get(params)] 284 | elsif params[:scroll] 285 | scroll_enumerator(client, response) 286 | else 287 | response.fetch("hits").fetch("hits") 288 | end 289 | end 290 | 291 | # @api private 292 | def response 293 | options[:response] || client.search(**params, body: body) 294 | end 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /lib/rom/elasticsearch/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/relation" 4 | 5 | require "rom/elasticsearch/index_name" 6 | require "rom/elasticsearch/relation/loaded" 7 | require "rom/elasticsearch/types" 8 | require "rom/elasticsearch/schema" 9 | require "rom/elasticsearch/attribute" 10 | 11 | module ROM 12 | module Elasticsearch 13 | # Elasticsearch relation API 14 | # 15 | # Provides access to indexed data, and methods for managing indices. 16 | # Works like a standard Relation, which means it's lazy and composable, 17 | # and has access to commands via `Relation#command`. 18 | # 19 | # Indices are configured based on two settings: 20 | # - `Relation#name.dataset` - which is configured in a standard way via `schema` block 21 | # - `Relation.index_settings` - which is a class-level setting 22 | # 23 | # Optionally, query DSL can be enabled via `:query_dsl` plugin. 24 | # 25 | # @example setting up a relation 26 | # class Pages < ROM::Relation[:elasticsearch] 27 | # schema do 28 | # attribute :id, Types::ID 29 | # attribute :title, Types.Keyword 30 | # attribute :body, Types.Text(analyzer: "snowball") 31 | # end 32 | # end 33 | # 34 | # @example using query DSL 35 | # class Users < ROM::Relation[:elasticsearch] 36 | # use :query_dsl 37 | # 38 | # schema do 39 | # # ... 40 | # end 41 | # end 42 | # 43 | # users.search do 44 | # query do 45 | # match name: "Jane" 46 | # end 47 | # end 48 | # 49 | # @api public 50 | class Relation < ROM::Relation 51 | include ROM::Elasticsearch 52 | 53 | adapter :elasticsearch 54 | 55 | # @!method self.index_settings 56 | # Manage the index_settings 57 | # 58 | # This is set by default to: 59 | # 60 | # ``` ruby 61 | # { number_of_shards: 1, 62 | # index: { 63 | # analysis: { 64 | # analyzer: { 65 | # standard_stopwords: { 66 | # type: "standard", 67 | # stopwords: "_english_" 68 | # } 69 | # } 70 | # } 71 | # } }.freeze 72 | # ``` 73 | # 74 | # @overload index_settings 75 | # Return the index_settings that the relation will use 76 | # @return [Hash] 77 | # 78 | # @overload index_settings(settings) 79 | # Set index settings 80 | # 81 | # @see https://www.elastic.co/guide/en/elasticsearch/guide/current/index-management.html 82 | # 83 | # @example 84 | # class Users < ROM::Relation[:elasticsearch] 85 | # index_settings( 86 | # # your custom settings 87 | # ) 88 | # 89 | # schema do 90 | # # ... 91 | # end 92 | # end 93 | # 94 | # @param [Hash] index_settings_hash 95 | defines :index_settings 96 | 97 | # @!method self.multi_index_types 98 | # Manage index types for multi-index search 99 | # 100 | # @overload multi_index_types 101 | # @return [Array] a list of index types 102 | # 103 | # @overload multi_index_types(types) 104 | # @return [Array] a list of index types 105 | # 106 | # @example 107 | # class Search < ROM::Relation[:elasticsearch] 108 | # multi_index_types :pages, :posts 109 | # 110 | # schema(multi: true) do 111 | # # define your schema 112 | # end 113 | # end 114 | defines :multi_index_types 115 | 116 | schema_class Elasticsearch::Schema 117 | schema_attr_class Elasticsearch::Attribute 118 | 119 | # Overridden output_schema, as we *always* want to use it, 120 | # whereas in core, it is only used when there's at least one read-type 121 | option :output_schema, default: -> { schema.to_output_hash } 122 | 123 | # @attribute [r] current_page 124 | # @return [Integer] Currently set page 125 | option :current_page, default: -> { 1 } 126 | 127 | # @attribute [r] per_page 128 | # @return [Integer] Number of results per page 129 | option :per_page, reader: false, optional: true, default: -> { 10 } 130 | 131 | # Default index settings that can be overridden 132 | index_settings( 133 | {number_of_shards: 1, 134 | index: { 135 | analysis: { 136 | analyzer: { 137 | standard_stopwords: { 138 | type: "standard", 139 | stopwords: "_english_" 140 | } 141 | } 142 | } 143 | }}.freeze 144 | ) 145 | 146 | # Define a schema for the relation 147 | # 148 | # @return [self] 149 | def self.schema(dataset = nil, multi: false, **opts, &block) 150 | if multi 151 | super(IndexName[:_all], **opts, &block) 152 | else 153 | super(dataset, **opts, &block) 154 | end 155 | end 156 | 157 | # Load a relation 158 | # 159 | # @return [Loaded] 160 | # 161 | # @api public 162 | def call 163 | Loaded.new(new(dataset.call)) 164 | end 165 | 166 | # Return a relation with changed sorting logic 167 | # 168 | # @param [Array] attrs 169 | # 170 | # @return [Relation] 171 | # 172 | # @api public 173 | def order(*attrs) 174 | new(dataset.sort(*schema.project(*attrs).map(&:to_sort_expr))) 175 | end 176 | 177 | # Return a relation with page number set 178 | # 179 | # @param [Integer] num 180 | # 181 | # @return [Relation] 182 | # 183 | # @api public 184 | def page(num) 185 | new(dataset.from((num - 1) * per_page), current_page: num) 186 | end 187 | 188 | # Return a relation with per-page number set 189 | # 190 | # @param [Integer] num 191 | # 192 | # @return [Relation] 193 | # 194 | # @api public 195 | def per_page(num = Undefined) 196 | if num.equal?(Undefined) 197 | options[:per_page] 198 | else 199 | new(dataset.size(num), per_page: num) 200 | end 201 | end 202 | 203 | # Return a relation with scroll set 204 | # 205 | # @param [String] ttl 206 | # 207 | # @return [Relation] 208 | # 209 | # @api public 210 | def scroll(ttl) 211 | new(dataset.scroll(ttl)) 212 | end 213 | 214 | # Map indexed data 215 | # 216 | # @yieldparam [Hash,ROM::Struct] 217 | # 218 | # @return [Array] 219 | # 220 | # @api public 221 | def map(&block) 222 | to_a.map(&block) 223 | end 224 | 225 | # Pluck specific attribute values 226 | # 227 | # @param [Symbol] name The name of the attribute 228 | # 229 | # @return [Array] 230 | # 231 | # @api public 232 | def pluck(name) 233 | map { |t| t[name] } 234 | end 235 | 236 | # Restrict indexed data by id 237 | # 238 | # @return [Relation] 239 | # 240 | # @api public 241 | def get(id) 242 | new(dataset.get(id)) 243 | end 244 | 245 | # Restrict relation data by a search query 246 | # 247 | # @example 248 | # users.search(query: { match: { name: "Jane" } }) 249 | # 250 | # @param [Hash] options Search options compatible with Elasticsearch::Client API 251 | # 252 | # @return [Relation] 253 | # 254 | # @api public 255 | def search(options) 256 | new(dataset.search(options)) 257 | end 258 | 259 | # Restrict relation data by a query search 260 | # 261 | # @example 262 | # users.query(match: { name: "Jane" }) 263 | # 264 | # @param [Hash] query Query options compatible with Elasticsearch::Client API 265 | # 266 | # @return [Relation] 267 | # 268 | # @api public 269 | def query(query) 270 | new(dataset.query(query)) 271 | end 272 | 273 | # Restrict relation data by a string-based query 274 | # 275 | # @example 276 | # users.query_string("name:'Jane'") 277 | # 278 | # @param [Hash] query Query string compatible with Elasticsearch::Client API 279 | # 280 | # @return [Relation] 281 | # 282 | # @api public 283 | def query_string(expr) 284 | new(dataset.query_string(expr)) 285 | end 286 | 287 | # Create relation's index in ES 288 | # 289 | # @return [Hash] raw response from the client 290 | # 291 | # @api public 292 | def create_index 293 | dataset.create_index(index_params) 294 | end 295 | 296 | # Delete relation's index in ES 297 | # 298 | # @return [Hash] raw response from the client 299 | # 300 | # @api public 301 | def delete_index 302 | dataset.delete_index 303 | end 304 | 305 | # Delete all indexed data from the current relation 306 | # 307 | # @return [Hash] raw response from the client 308 | # 309 | # @api public 310 | def delete 311 | dataset.delete 312 | end 313 | 314 | # Refresh indexed data 315 | # 316 | # @example 317 | # users.command(:create).call(id: 1, name: "Jane").refresh.to_a 318 | # 319 | # @return [Relation] 320 | # 321 | # @api public 322 | def refresh 323 | new(dataset.refresh) 324 | end 325 | 326 | # Return count of documents in the index 327 | # 328 | # @example 329 | # users.count 330 | # GET /_count 331 | # 332 | # @example 333 | # users.search().count 334 | # GET /_count?q= 335 | # 336 | # @return [Integer] 337 | # 338 | # @api public 339 | def count 340 | dataset.client.count( 341 | index: dataset.index, 342 | body: dataset.body 343 | )["count"] 344 | end 345 | 346 | # Restrict relation data by offset 347 | # 348 | # @example 349 | # users.search.from(100) 350 | # GET /_search?from=100 351 | # 352 | # @return [Integer] 353 | # 354 | # @api public 355 | def from(value) 356 | new(dataset.from(value)) 357 | end 358 | 359 | # Restrict relation data by limit the count of documents 360 | # 361 | # @example 362 | # users.search.size(100) 363 | # GET /_search?size=100 364 | # 365 | # @return [Integer] 366 | # 367 | # @api public 368 | def size(value) 369 | new(dataset.size(value)) 370 | end 371 | 372 | private 373 | 374 | # Dataset index params based on relation configuration 375 | # 376 | # @return [Hash] 377 | # 378 | # @api private 379 | def index_params 380 | {index: name.dataset.to_sym, 381 | body: { 382 | settings: self.class.index_settings, 383 | mappings: {properties: schema.to_properties} 384 | }} 385 | end 386 | end 387 | end 388 | end 389 | --------------------------------------------------------------------------------