{"id":13878846,"url":"https://github.com/mrkamel/search_flip","last_synced_at":"2025-05-16T07:04:43.184Z","repository":{"id":16314058,"uuid":"44699485","full_name":"mrkamel/search_flip","owner":"mrkamel","description":"Full-Featured ElasticSearch Ruby Client with a Chainable DSL","archived":false,"fork":false,"pushed_at":"2024-07-08T13:02:15.000Z","size":645,"stargazers_count":325,"open_issues_count":2,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-05-09T20:19:08.552Z","etag":null,"topics":["chainable","elasticsearch","fulltext-indices","fulltext-search","library","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrkamel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-10-21T19:33:27.000Z","updated_at":"2025-05-09T19:33:25.000Z","dependencies_parsed_at":"2024-11-20T10:55:27.304Z","dependency_job_id":"fa379419-10ea-463e-a975-fa62d221509f","html_url":"https://github.com/mrkamel/search_flip","commit_stats":{"total_commits":453,"total_committers":6,"mean_commits":75.5,"dds":"0.45253863134657835","last_synced_commit":"4705bd3b373e493faf62d404877d766b26c0df6e"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkamel%2Fsearch_flip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkamel%2Fsearch_flip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkamel%2Fsearch_flip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkamel%2Fsearch_flip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrkamel","download_url":"https://codeload.github.com/mrkamel/search_flip/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254485055,"owners_count":22078767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chainable","elasticsearch","fulltext-indices","fulltext-search","library","ruby"],"created_at":"2024-08-06T08:02:01.787Z","updated_at":"2025-05-16T07:04:38.170Z","avatar_url":"https://github.com/mrkamel.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"\n# search_flip\n\n**Full-Featured Elasticsearch Ruby Client with a Chainable DSL**\n\n[![Build](https://github.com/mrkamel/search_flip/workflows/test/badge.svg)](https://github.com/mrkamel/search_flip/actions?query=workflow%3Atest+branch%3Amaster)\n[![Gem Version](https://badge.fury.io/rb/search_flip.svg)](http://badge.fury.io/rb/search_flip)\n\nUsing SearchFlip it is dead-simple to create index classes that correspond to\n[Elasticsearch](https://www.elastic.co/) indices and to manipulate, query and\naggregate these indices using a chainable, concise, yet powerful DSL. Finally,\nSearchFlip supports Elasticsearch 2.x, 5.x, 6.x, 7.x and 8.x as well as\nOpensearch 1.x and 2.x. Check section [Feature Support](#feature-support) for\nversion dependent features.\n\n```ruby\nCommentIndex.search(\"hello world\", default_field: \"title\").where(visible: true).aggregate(:user_id).sort(id: \"desc\")\n\nCommentIndex.aggregate(:user_id) do |aggregation|\n  aggregation.aggregate(histogram: { date_histogram: { field: \"created_at\", interval: \"month\" }})\nend\n\nCommentIndex.range(:created_at, gt: Date.today - 1.week, lt: Date.today).where(state: [\"approved\", \"pending\"])\n```\n\n## Updating from previous SearchFlip versions\n\nCheckout [UPDATING.md](./UPDATING.md) for detailed instructions.\n\n## Comparison with other gems\n\nThere are great ruby gems to work with Elasticsearch like e.g. searchkick and\nelasticsearch-ruby already. However, they don't have a chainable API. Compare\nyourself.\n\n```ruby\n# elasticsearch-ruby\nComment.search(\n  query: {\n    query_string: {\n      query: \"hello world\",\n      default_operator: \"AND\"\n    }\n  }\n)\n\n# searchkick\nComment.search(\"hello world\", where: { available: true }, order: { id: \"desc\" }, aggs: [:username])\n\n# search_flip\nCommentIndex.search(\"hello world\").where(available: true).sort(id: \"desc\").aggregate(:username)\n\n```\n\nFinally, SearchFlip comes with a minimal set of dependencies.\n\n## Reference Docs\n\nSearchFlip has a great documentation.\nCheck youself at [http://www.rubydoc.info/github/mrkamel/search_flip](http://www.rubydoc.info/github/mrkamel/search_flip)\n\n## Install\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'search_flip'\n```\n\nand then execute\n\n```\n$ bundle\n```\n\nor install it via\n\n```\n$ gem install search_flip\n```\n\n## Config\n\nYou can change global config options like:\n\n```ruby\nSearchFlip::Config[:environment] = \"development\"\nSearchFlip::Config[:base_url] = \"http://127.0.0.1:9200\"\n```\n\nAvailable config options are:\n\n* `index_prefix` to have a prefix added to your index names automatically. This\n  can be useful to separate the indices of e.g. testing and development environments.\n* `base_url` to tell SearchFlip how to connect to your cluster\n* `bulk_limit` a global limit for bulk requests\n* `bulk_max_mb` a global limit for the payload of bulk requests\n* `auto_refresh` tells SearchFlip to automatically refresh an index after\n  import, index, delete, etc operations. This is e.g. useful for testing, etc.\n  Defaults to false.\n* `version` allows to configure the elasticsearch version no matter which\n  elasticsearch version is actually in use. The version information is needed to\n  support version dependent features. Please note that manually configuring the\n  version is usually not need as the version by default is determined by sending\n  one request to elasticsearch.\n\n```ruby\nSearchFlip::Config[:version] = { number: \"8.1.1\" }\nSearchFlip::Config[:version] = { number: \"2.13\", distribution: \"opensearch\" }\n```\n\n## Usage\n\nFirst, create a separate class for your index and include `SearchFlip::Index`.\n\n```ruby\nclass CommentIndex\n  include SearchFlip::Index\nend\n```\n\nThen tell the Index about the index name, the corresponding model and how to\nserialize the model for indexing.\n\n```ruby\nclass CommentIndex\n  include SearchFlip::Index\n\n  def self.index_name\n    \"comments\"\n  end\n\n  def self.model\n    Comment\n  end\n\n  def self.serialize(comment)\n    {\n      id: comment.id,\n      username: comment.username,\n      title: comment.title,\n      message: comment.message\n    }\n  end\nend\n```\n\nOptionally, you can specify a custom `type_name`, but note that starting with\nElasticsearch 7, types are deprecated.\n\n```ruby\nclass CommentIndex\n  # ...\n\n  def self.type_name\n    \"comment\"\n  end\nend\n```\n\nYou can additionally specify an `index_scope` which will automatically be\napplied to scopes, eg. ActiveRecord::Relation objects, passed to `#import`,\n`#index`, etc. This can be used to preload associations that are used when\nserializing records or to restrict the records you want to index.\n\n```ruby\nclass CommentIndex\n  # ...\n\n  def self.index_scope(scope)\n    scope.preload(:user)\n  end\nend\n\nCommentIndex.import(Comment.all) # =\u003e CommentIndex.import(Comment.all.preload(:user))\n```\n\nTo specify a custom mapping:\n\n```ruby\nclass CommentIndex\n  # ...\n\n  def self.mapping\n    {\n      properties: {\n        # ...\n      }\n    }\n  end\n\n  # ...\nend\n```\n\nPlease note that you need to specify the mapping without a type name, even for\nElasticsearch versions before 7, as SearchFlip will add the type name\nautomatically if neccessary.\n\nTo specify index settings:\n\n```ruby\ndef self.index_settings\n  {\n    settings: {\n      number_of_shards: 10,\n      number_of_replicas: 2\n    }\n  }\nend\n```\n\nThen you can interact with the index:\n\n```ruby\nCommentIndex.create_index\nCommentIndex.index_exists?\nCommentIndex.delete_index\nCommentIndex.update_mapping\nCommentIndex.close_index\nCommentIndex.open_index\n```\n\nIndex records (automatically uses the [Bulk API]):\n\n```ruby\nCommentIndex.import(Comment.all)\nCommentIndex.import(Comment.first)\nCommentIndex.import([Comment.find(1), Comment.find(2)])\nCommentIndex.import(Comment.where(\"created_at \u003e ?\", Time.now - 7.days))\n```\n\nQuery records:\n\n```ruby\nCommentIndex.total_entries\n# =\u003e 2838\n\nCommentIndex.search(\"title:hello\").records\n# =\u003e [#\u003cComment ...\u003e, #\u003cComment ...\u003e, ...]\n\nCommentIndex.where(username: \"mrkamel\").total_entries\n# =\u003e 13\n\nCommentIndex.aggregate(:username).aggregations(:username)\n# =\u003e {1=\u003e#\u003cSearchFlip::Result doc_count=37 ...\u003e, 2=\u003e... }\n...\n```\n\nPlease note that you can check the request that will be send to Elasticsearch\nby calling `#request` on the query:\n\n```ruby\nCommentIndex.search(\"hello world\").sort(id: \"desc\").aggregate(:username).request\n# =\u003e {:query=\u003e{:bool=\u003e{:must=\u003e[{:query_string=\u003e{:query=\u003e\"hello world\", :default_operator=\u003e:AND}}]}}, ...}\n```\n\nDelete records:\n\n```ruby\n# for Elasticsearch \u003e= 2.x and \u003c 5.x, the delete-by-query plugin is required\n# for the following query:\n\nCommentIndex.match_all.delete\n\n# or delete manually via the bulk API:\n\nCommentIndex.bulk do |indexer|\n  CommentIndex.match_all.find_each do |record|\n    indexer.delete record.id\n  end\nend\n```\n\nWhen indexing or deleting documents, you can pass options to control the bulk\nindexing and you can use all options provided by the [Bulk API]:\n\n```ruby\nCommentIndex.import(Comment.first, { bulk_limit: 1_000 }, op_type: \"create\", routing: \"routing_key\")\n\n# or directly\n\nCommentIndex.create(Comment.first, { bulk_max_mb: 100 }, routing: \"routing_key\")\nCommentIndex.update(Comment.first, ...)\n```\n\nCheckout the Elasticsearch [Bulk API] docs for more info as well as\n[SearchFlip::Bulk](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/Bulk)\nfor a complete list of available options to control the bulk indexing of\nSearchFlip.\n\n## Working with Elasticsearch Aliases\n\nYou can use and manage Elasticsearch Aliases like the following:\n\n```ruby\nclass UserIndex\n  include SearchFlip::Index\n\n  def self.index_name\n    alias_name\n  end\n\n  def self.alias_name\n    \"users\"\n  end\nend\n```\n\nThen, create an index, import the records and add the alias like:\n\n```ruby\nnew_user_index = UserIndex.with_settings(index_name: \"users-#{SecureRandom.hex}\")\nnew_user_index.create_index\nnew_user_index.import User.all\nnew_user.connection.update_aliases(actions: [\n  add: { index: new_user_index.index_name, alias: new_user_index.alias_name }\n])\n```\n\nIf the alias already exists, you have to remove it as well first within `update_aliases`.\n\nPlease note: `with_settings(index_name: '...')` returns an anonymous (i.e.\ntemporary) class which inherits from UserIndex and overwrites `index_name`.\n\n## Chainable Methods\n\nSearchFlip supports even more advanced usages, like e.g. post filters, filtered\naggregations or nested aggregations via simple to use API methods.\n\n### Query/Filter Criteria Methods\n\nSearchFlip provides powerful methods to query/filter Elasticsearch:\n\n* `where`\n\nThe `.where` method feels like ActiveRecord's `where` and adds a bool filter clause to the request:\n\n```ruby\nCommentIndex.where(reviewed: true)\nCommentIndex.where(likes: 0 .. 10_000)\nCommentIndex.where(state: [\"approved\", \"rejected\"])\n```\n\n* `where_not`\n\nThe `.where_not` method is like `.where`, but excluding the matching documents:\n\n```ruby\nCommentIndex.where_not(id: [1, 2, 3])\n```\n\n* `range`\n\nUse `.range` to add a range filter query:\n\n```ruby\nCommentIndex.range(:created_at, gt: Date.today - 1.week, lt: Date.today)\n```\n\n* `filter`\n\nUse `.filter` to add raw filter queries:\n\n```ruby\nCommentIndex.filter(term: { state: \"approved\" })\n```\n\n* `should`\n\nUse `.should` to add raw should queries:\n\n```ruby\nCommentIndex.should([\n  { term: { state: \"approved\" } },\n  { term: { user: \"mrkamel\" } },\n])\n```\n\n* `must`\n\nUse `.must` to add raw must queries:\n\n```ruby\nCommentIndex.must(term: { state: \"approved\" })\n```\n\n* `must_not`\n\nLike `must`, but excluding the matching documents:\n\n```ruby\nCommentIndex.must_not(term: { state: \"approved\" })\n```\n\n* `search`\n\nAdds a query string query, with AND as default operator:\n\n```ruby\nCommentIndex.search(\"hello world\")\nCommentIndex.search(\"state:approved\")\nCommentIndex.search(\"username:a*\")\nCommentIndex.search(\"state:approved OR state:rejected\")\nCommentIndex.search(\"hello world\", default_operator: \"OR\")\n```\n\n* `exists`\n\nUse `exists` to add an `exists` query:\n\n```ruby\nCommentIndex.exists(:state)\n```\n\n* `exists_not`\n\nLike `exists`, but excluding the matching documents:\n\n```ruby\nCommentIndex.exists_not(:state)\n```\n\n* `match_all`\n\nSimply matches all documents:\n\n```ruby\nCommentIndex.match_all\n```\n\n* `match_none`\n\nSimply matches none documents at all:\n\n```ruby\nCommentIndex.match_none\n```\n\n* `all`\n\nSimply returns the criteria as is or an empty criteria when called on the index\nclass directly. Useful for chaining.\n\n```ruby\nCommentIndex.all\n```\n\n* `to_query`\n\nSometimes, you want to convert the constraints of a search flip query to a raw\nquery to e.g. use it in a should clause:\n\n```ruby\nCommentIndex.should([\n  CommentIndex.range(:likes_count, gt: 10).to_query,\n  CommentIndex.search(\"search term\").to_query\n])\n```\n\nIt returns all added queries and filters, including post filters as a raw\nquery:\n\n```ruby\nCommentIndex.where(state: \"new\").search(\"text\").to_query\n# =\u003e {:bool=\u003e{:filter=\u003e[{:term=\u003e{:state=\u003e\"new\"}}], :must=\u003e[{:query_string=\u003e{:query=\u003e\"text\", ...}}]}}\n```\n\n### Post Query/Filter Criteria Methods\n\nAll query/filter criteria methods (`#where`, `#where_not`, `#range`, etc.) are available\nin post filter mode as well, ie. filters/queries applied after aggregations\nare calculated. Checkout the Elasticsearch docs for further info.\n\n```ruby\nquery = CommentIndex.aggregate(:user_id)\nquery = query.post_where(reviewed: true)\nquery = query.post_search(\"username:a*\")\n```\n\nCheckout [PostFilterable](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/PostFilterable)\nfor a complete API reference.\n\n### Aggregations\n\nSearchFlip allows to elegantly specify nested aggregations, no matter how deeply\nnested:\n\n```ruby\nquery = OrderIndex.aggregate(:username, order: { revenue: \"desc\" }) do |aggregation|\n  aggregation.aggregate(revenue: { sum: { field: \"price\" }})\nend\n```\n\nGenerally, aggregation results returned by Elasticsearch are returned as a\n`SearchFlip::Result`, which basically is a `Hashie::Mash`, such that you can\naccess them via:\n\n```ruby\nquery.aggregations(:username)[\"mrkamel\"].revenue.value\n```\n\nStill, if you want to get the raw aggregations returned by Elasticsearch,\naccess them without supplying any aggregation name to `#aggregations`:\n\n```ruby\nquery.aggregations # =\u003e returns the raw aggregation section\n\nquery.aggregations[\"username\"][\"buckets\"].detect { |bucket| bucket[\"key\"] == \"mrkamel\" }[\"revenue\"][\"value\"] # =\u003e 238.50\n```\n\nOnce again, the criteria methods (`#where`, `#range`, etc.) are available in\naggregations as well:\n\n```ruby\nquery = OrderIndex.aggregate(average_price: {}) do |aggregation|\n  aggregation = aggregation.match_all\n  aggregation = aggregation.where(user_id: current_user.id) if current_user\n\n  aggregation.aggregate(average_price: { avg: { field: \"price\" }})\nend\n\nquery.aggregations(:average_price).average_price.value\n```\n\nEven various criteria for top hits aggregations can be specified elegantly:\n\n```ruby\nquery = ProductIndex.aggregate(sponsored: { top_hits: {} }) do |aggregation|\n  aggregation.sort(:rank).highlight(:title).source([:id, :title])\nend\n```\n\nCheckout [Aggregatable](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/Aggregatable)\nas well as [Aggregation](http://www.rubydoc.info/github/mrkamel/search_flip/SearchFlip/Aggregation)\nfor a complete API reference.\n\n### Suggestions\n\n```ruby\nquery = CommentIndex.suggest(:suggestion, text: \"helo\", term: { field: \"message\" })\nquery.suggestions(:suggestion).first[\"text\"] # =\u003e \"hello\"\n```\n\n### Highlighting\n\n```ruby\nCommentIndex.highlight([:title, :message])\nCommentIndex.highlight(:title).highlight(:description)\nCommentIndex.highlight(:title, require_field_match: false)\nCommentIndex.highlight(title: { type: \"fvh\" })\n```\n\n```ruby\nquery = CommentIndex.highlight(:title).search(\"hello\")\nquery.results[0]._hit.highlight.title # =\u003e \"\u003cem\u003ehello\u003c/em\u003e world\"\n```\n\n### Other Criteria Methods\n\nThere are even more chainable criteria methods to make your life easier. For a\nfull list, checkout the reference docs.\n\n* `source`\n\nIn case you want to restrict the returned fields, simply specify\nthe fields via `#source`:\n\n```ruby\nCommentIndex.source([:id, :message]).search(\"hello world\")\n```\n\n* `paginate`, `page`, `per`\n\nSearchFlip supports\n[will_paginate](https://github.com/mislav/will_paginate) and\n[kaminari](https://github.com/kaminari/kaminari) compatible pagination. Thus,\nyou can either use `#paginate` or `#page` in combination with `#per`:\n\n```ruby\nCommentIndex.paginate(page: 3, per_page: 50)\nCommentIndex.page(3).per(50)\n```\n\n* `profile`\n\nUse `#profile` to enable query profiling:\n\n```ruby\nquery = CommentIndex.profile(true)\nquery.raw_response[\"profile\"] # =\u003e { \"shards\" =\u003e ... }\n```\n\n* `preload`, `eager_load` and `includes`\n\nUses the well known methods from ActiveRecord to load\nassociated database records when fetching the respective\nrecords themselves. Works with other ORMs as well, if\nsupported.\n\nUsing `#preload`:\n\n```ruby\nCommentIndex.preload(:user, :post).records\nPostIndex.includes(comments: :user).records\n```\n\nor `#eager_load`\n\n```ruby\nCommentIndex.eager_load(:user, :post).records\nPostIndex.eager_load(comments: :user).records\n```\n\nor `#includes`\n\n```ruby\nCommentIndex.includes(:user, :post).records\nPostIndex.includes(comments: :user).records\n```\n\n* `find_in_batches`\n\nUsed to fetch and yield records in batches using the ElasicSearch scroll API.\nThe batch size and scroll API timeout can be specified.\n\n```ruby\nCommentIndex.search(\"hello world\").find_in_batches(batch_size: 100) do |batch|\n  # ...\nend\n```\n\n* `find_results_in_batches`\n\nUsed like `find_in_batches`, but yielding the raw results (as\n`SearchFlip::Result` objects) instead of database records.\n\n```ruby\nCommentIndex.search(\"hello world\").find_results_in_batches(batch_size: 100) do |batch|\n  # ...\nend\n```\n\n* `find_each`\n\nLike `#find_in_batches` but yielding one record at a time.\n\n```ruby\nCommentIndex.search(\"hello world\").find_each(batch_size: 100) do |record|\n  # ...\nend\n```\n\n* `find_each_result`\n\nLike `#find_results_in_batches`, but yielding one record at a time.\n\n```ruby\nCommentIndex.search(\"hello world\").find_each_result(batch_size: 100) do |batch|\n  # ...\nend\n```\n\n* `scroll`\n\nYou can as well use the underlying scroll API directly, ie. without using higher\nlevel scrolling:\n\n```ruby\nquery = CommentIndex.scroll(timeout: \"5m\")\n\nuntil query.records.empty?\n  # ...\n\n  query = query.scroll(id: query.scroll_id, timeout: \"5m\")\nend\n```\n\n* `failsafe`\n\nUse `#failsafe` to prevent any exceptions from being raised for query string\nsyntax errors or Elasticsearch being unavailable, etc.\n\n```ruby\nCommentIndex.search(\"invalid/request\").execute\n# raises SearchFlip::ResponseError\n\n# ...\n\nCommentIndex.search(\"invalid/request\").failsafe(true).execute\n# =\u003e #\u003cSearchFlip::Response ...\u003e\n```\n\n* `merge`\n\nYou can merge criterias, ie. combine the attributes (constraints, settings,\netc) of two individual criterias:\n\n```ruby\nCommentIndex.where(approved: true).merge(CommentIndex.search(\"hello\"))\n# equivalent to: CommentIndex.where(approved: true).search(\"hello\")\n```\n\n* `timeout`\n\nSpecify a timeout to limit query processing time:\n\n```ruby\nCommentIndex.timeout(\"3s\").execute\n```\n\n* `http_timeout`\n\nSpecify a http timeout for the request which will be send to Elasticsearch:\n\n```ruby\nCommentIndex.http_timeout(3).execute\n```\n\n* `terminate_after`\n\nActivate early query termination to stop query processing after the specified\nnumber of records has been found:\n\n```ruby\nCommentIndex.terminate_after(10).execute\n```\n\nFor further details and a full list of methods, check out the reference docs.\n\n* `custom`\n\nYou can add a custom clause to the request via `custom`\n\n```ruby\nCommentIndex.custom(custom_clause: '...')\n```\n\nThis can be useful for Elasticsearch features not yet supported via criteria\nmethods by SearchFlip, custom plugin clauses, etc.\n\n### Custom Criteria Methods\n\nTo add custom criteria methods, you can add class methods to your index class.\n\n```ruby\nclass HotelIndex\n  # ...\n\n  def self.where_geo(lat:, lon:, distance:)\n    filter(geo_distance: { distance: distance, location: { lat: lat, lon: lon } })\n  end\nend\n\nHotelIndex.search(\"bed and breakfast\").where_geo(lat: 53.57532, lon: 10.01534, distance: '50km').aggregate(:rating)\n```\n\n## Using multiple Elasticsearch clusters\n\nTo use multiple Elasticsearch clusters, specify a connection within your\nindices:\n\n```ruby\nMyConnection = SearchFlip::Connection.new(base_url: \"http://elasticsearch.host:9200\")\n\nclass MyIndex\n  include SearchFlip::Index\n\n  def self.connection\n    MyConnection\n  end\nend\n```\n\nThis allows to use different clusters per index e.g. when migrating indices to\nnew versions of Elasticsearch.\n\nYou can specify basic auth, additional headers, request timeouts, etc via:\n\n```ruby\nhttp_client = SearchFlip::HTTPClient.new\n\n# Basic Auth\nhttp_client = http_client.basic_auth(user: \"username\", pass: \"password\")\n\n# Raw Auth Header\nhttp_client = http_client.auth(\"Bearer VGhlIEhUVFAgR2VtLCBST0NLUw\")\n\n# Proxy Settings\nhttp_client = http_client.via(\"proxy.host\", 8080)\n\n# Custom headers\nhttp_client = http_client.headers(key: \"value\")\n\n# Timeouts\nhttp_client = http_client.timeout(20)\n\nSearchFlip::Connection.new(base_url: \"...\", http_client: http_client)\n```\n\n## AWS Elasticsearch / Signed Requests\n\nTo use SearchFlip with AWS Elasticsearch and signed requests, you have to add\n`aws-sdk-core` to your Gemfile and tell SearchFlip to use the\n`SearchFlip::AwsSigv4Plugin`:\n\n```ruby\nrequire \"search_flip/aws_sigv4_plugin\"\n\nMyConnection = SearchFlip::Connection.new(\n  base_url: \"https://your-elasticsearch-cluster.es.amazonaws.com\",\n  http_client: SearchFlip::HTTPClient.new(\n    plugins: [\n      SearchFlip::AwsSigv4Plugin.new(\n        region: \"...\",\n        access_key_id: \"...\",\n        secret_access_key: \"...\"\n      )\n    ]\n  )\n)\n```\n\nAgain, in your index you need to specify this connection:\n\n```ruby\nclass MyIndex\n  include SearchFlip::Index\n\n  def self.connection\n    MyConnection\n  end\nend\n```\n\n## Routing and other index-time options\n\nOverride `index_options` in case you want to use routing or pass other\nindex-time options:\n\n```ruby\nclass CommentIndex\n  include SearchFlip::Index\n\n  def self.index_options(comment)\n    {\n      routing: comment.user_id,\n      version: comment.version,\n      version_type: \"external_gte\"\n    }\n  end\nend\n```\n\nThese options will be passed whenever records get indexed, deleted, etc.\n\n## Instrumentation\n\nSearchFlip supports instrumentation for request execution via\n`ActiveSupport::Notifications` compatible instrumenters to e.g. allow global\nperformance tracing, etc.\n\nTo use instrumentation, configure the instrumenter:\n\n```ruby\nSearchFlip::Config[:instrumenter] = ActiveSupport::Notifications\n```\n\nSubsequently, you can subscribe to notifcations for `request.search_flip`:\n\n```ruby\nActiveSupport::Notifications.subscribe(\"request.search_flip\") do |name, start, finish, id, payload|\n  payload[:index] # the index class\n  payload[:request] # the request hash sent to Elasticsearch\n  payload[:response] # the SearchFlip::Response object or nil in case of errors\nend\n```\n\nA notification will be send for every request that is sent to Elasticsearch.\n\n## Non-ActiveRecord models\n\nSearchFlip ships with built-in support for ActiveRecord models, but using\nnon-ActiveRecord models is very easy. The model must implement a `find_each`\nclass method and the Index class needs to implement `Index.record_id` and\n`Index.fetch_records`. The default implementations for the index class are as\nfollows:\n\n```ruby\nclass MyIndex\n  include SearchFlip::Index\n\n  def self.record_id(object)\n    object.id\n  end\n\n  def self.fetch_records(ids)\n    model.where(id: ids)\n  end\nend\n```\n\nThus, if your ORM supports `.find_each`, `#id` and `#where` you are already\ngood to go. Otherwise, simply add your custom implementation of those methods\nthat work with whatever ORM you use.\n\n## JSON\n\nSearchFlip is using the [Oj gem](https://github.com/ohler55/oj) to generate\nJSON. More concretely, SearchFlip is using:\n\n```ruby\nOj.dump({ key: \"value\" }, mode: :custom, use_to_json: true, time_format: :xmlschema, bigdecimal_as_decimal: false)\n```\n\nThe `use_to_json` option is used for maximum compatibility, most importantly\nwhen using rails `ActiveSupport::TimeWithZone` timestamps, which `oj` can not\nserialize natively. However, `use_to_json` adds performance overhead. You can\nchange the json options via:\n\n```ruby\nSearchFlip::Config[:json_options] = {\n  mode: :custom,\n  use_to_json: false,\n  time_format: :xmlschema,\n  bigdecimal_as_decimal: false\n}\n```\n\nHowever, you then have to convert timestamps manually for indexation via e.g.:\n\n```ruby\nclass MyIndex\n  # ...\n\n  def self.serialize(model)\n    {\n      # ...\n\n      created_at: model.created_at.to_time\n    }\n  end\nend\n```\n\nPlease check out the oj docs for more details.\n\n## Feature Support\n\n* for Elasticsearch 2.x, the delete-by-query plugin is required to delete\n  records via queries\n* `#match_none` is only available with Elasticsearch \u003e= 5\n* `#track_total_hits` is only available with Elasticsearch \u003e= 7\n\n## Keeping your Models and Indices in Sync\n\nBesides the most basic approach to get you started, SearchFlip currently doesn't\nship with any means to automatically keep your models and indices in sync,\nbecause every method is very much bound to the concrete environment and depends\non your concrete requirements. In addition, the methods to achieve model/index\nconsistency can get arbitrarily complex and we want to keep this bloat out of\nthe SearchFlip codebase.\n\n```ruby\nclass Comment \u003c ActiveRecord::Base\n  include SearchFlip::Model\n\n  notifies_index(CommentIndex)\nend\n```\n\nIt uses `after_commit` (if applicable, `after_save`, `after_destroy` and\n`after_touch` otherwise) hooks to synchronously update the index when your\nmodel changes.\n\n## Semantic Versioning\n\nSearchFlip is using Semantic Versioning: [SemVer](http://semver.org/)\n\n## Links\n\n* Elasticsearch: [https://www.elastic.co/](https://www.elastic.co/)\n* Reference Docs: [http://www.rubydoc.info/github/mrkamel/search_flip](http://www.rubydoc.info/github/mrkamel/search_flip)\n* will_paginate: [https://github.com/mislav/will_paginate](https://github.com/mislav/will_paginate)\n* kaminari: [https://github.com/kaminari/kaminari](https://github.com/kaminari/kaminari)\n* Oj: [https://github.com/ohler55/oj](https://github.com/ohler55/oj)\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## Running the test suite\n\nRunning the tests is super easy. The test suite uses sqlite, such that you only\nneed to install Elasticsearch. You can install Elasticsearch on your own, or\nyou can e.g. use docker-compose:\n\n```\n$ cd search_flip\n$ sudo ES_IMAGE=elasticsearch:5.4 docker-compose up\n$ rspec\n```\n\nThat's it.\n\n[Bulk API]: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrkamel%2Fsearch_flip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrkamel%2Fsearch_flip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrkamel%2Fsearch_flip/lists"}