Elasticsearch tarjoaa tehokkaan, RESTful-HTTP-käyttöliittymän tietojen indeksointiin ja kyselyihin, joka on rakennettu Apache Lucene kirjasto. Heti alusta alkaen se tarjoaa skaalattavan, tehokkaan ja vankan haun UTF-8-tuella. Se on tehokas työkalu indeksoimaan ja kyselemään valtavia määriä strukturoitua dataa ja täällä ApeeScape , se käyttää alustahakua ja sitä käytetään pian myös automaattiseen täydennykseen. Olemme valtavia faneja.
Chewy laajentaa Elasticsearch-Ruby -asiakasta, tehden siitä tehokkaamman ja tiukemman integraation Railsin kanssa.Koska alusta on rakennettu käyttäen Ruby on Rails , Elasticsearch-integraatiomme hyödyntää elasticsearch-ruby projekti (Ruby-integraatiokehys Elasticsearchille, joka tarjoaa asiakkaan yhteyden muodostamiseen Elasticsearch-klusteriin, Ruby-sovellusliittymä Elasticsearchin REST-sovellusliittymään sekä erilaisia laajennuksia ja apuohjelmia). Tämän perustan pohjalta olemme kehittäneet ja julkaisseet oman parannuksemme (ja yksinkertaistuksemme) Elasticsearch -sovelluksen hakuarkkitehtuuriin, joka on pakattu nimeämämme Ruby-helmi Sitkeä (esimerkkisovelluksen kanssa tässä ).
Chewy laajentaa Elasticsearch-Ruby -asiakasta, tehden siitä tehokkaamman ja tiukemman integraation Railsin kanssa. Tässä Elasticsearch-oppaassa keskustelen (käyttöesimerkkien avulla) siitä, miten saavutimme tämän, mukaan lukien toteutuksen aikana syntyneet tekniset esteet.
Vain muutama nopea muistiinpano ennen kuin jatkat oppaaseen:
Elasticsearchin skaalautuvuudesta ja tehokkuudesta huolimatta sen integrointi Railsiin ei osoittautunut aivan yhtä yksinkertaiseksi kuin odotettiin. ApeeScapessa huomasimme, että meidän on huomattavasti lisättävä Elasticsearch-Ruby-perussovellusta, jotta se olisi suorituskykyisempi ja tukisi lisätoimintoja.
Huolimatta Elasticsearchin skaalautuvuudesta ja tehokkuudesta, sen integrointi Railsiin ei osoittautunut aivan yhtä yksinkertaiseksi kuin odotettiin.Ja näin syntyi Chewy-helmi.
Muutamia erityisen huomionarvoisia Chewyn ominaisuuksia ovat:
Jokainen indeksi on havaittavissa kaikissa siihen liittyvissä malleissa.
Useimmat indeksoidut mallit liittyvät toisiinsa. Ja joskus on välttämätöntä denormalisoida nämä liittyvät tiedot ja sitoa ne samaan objektiin (esim. Jos haluat indeksoida joukon tunnisteita yhdessä niihin liittyvän artikkelin kanssa). Chewyn avulla voit määrittää päivitettävän hakemiston jokaiselle mallille, joten vastaavat artikkelit indeksoidaan uudelleen aina, kun asiaankuuluva tunniste päivitetään.
Hakemistoluokat ovat riippumattomia ORM / ODM-malleista.
Tämän parannuksen avulla esimerkiksi mallien välisen automaattisen täydennyksen toteuttaminen on paljon helpompaa. Voit vain määrittää hakemiston ja työskennellä sen kanssa olio-tavalla. Toisin kuin muut asiakkaat, Chewy-helmi poistaa tarpeen luoda hakemistoluokkia, tietojen tuonnin takaisinsoittoja ja muita komponentteja manuaalisesti.
Joukkotuonti on joka paikassa .
Chewy käyttää suurta Elasticsearch-sovellusliittymää täydelliseen uudelleenindeksointiin ja hakemistopäivityksiin. Se hyödyntää myös atomipäivitysten käsitettä, kerää muuttuneet objektit atomilohkossa ja päivittää ne kaikki kerralla.
Chewy tarjoaa AR-tyylisen kyselyn DSL.
Koska tämä parannus on ketjutettava, yhdistettävissä ja laiska, kyselyt voidaan tuottaa tehokkaammin.
Selvä, joten katsotaanpa, miten tämä kaikki toimii helmi ...
Elasticsearchilla on useita asiakirjaan liittyviä käsitteitä. Ensimmäinen on index
(analoginen database
in RDBMS ), joka koostuu joukosta documents
, joka voi olla useita types
(missä type
on eräänlainen RDBMS-taulukko).
Jokaisessa asiakirjassa on joukko fields
. Jokainen kenttä analysoidaan itsenäisesti ja sen analyysivaihtoehdot tallennetaan mapping
-kenttään sen tyypin mukaan. Chewy käyttää tätä rakennetta 'sellaisenaan' objektimallissaan:
class EntertainmentIndex { author.name } field :author_id, type: 'integer' field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end {movie: Video.movies, cartoon: Video.cartoons}.each do |type_name, scope| define_type scope.includes(:director, :tags), name: type_name do field :title, analyzer: 'title' field :year, type: 'integer' field :author, value: ->{ director.name } field :author_id, type: 'integer', value: ->{ director_id } field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end end end
Edellä määriteltiin Elasticsearch-indeksi nimeltä entertainment
kolmella tyypillä: book
, movie
ja cartoon
. Kullekin tyypille määriteltiin joitain kenttäkartoituksia ja hash-asetuksia koko hakemistolle.
Joten olemme määrittäneet EntertainmentIndex
ja haluamme suorittaa joitain kyselyjä. Ensimmäisessä vaiheessa meidän on luotava hakemisto ja tuotava tietomme:
EntertainmentIndex.create! EntertainmentIndex.import # EntertainmentIndex.reset! (which includes deletion, # creation, and import) could be used instead
.import
method on tietoinen tuotuista tiedoista, koska läpäisimme laajuudet määritellessämme tyyppejä; Siten se tuo kaikki pysyvään varastoon tallennetut kirjat, elokuvat ja sarjakuvat.
Kun tämä on tehty, voimme tehdä joitain kyselyjä:
EntertainmentIndex.query(match: {author: 'Tarantino'}).filter{ year > 1990 } EntertainmentIndex.query(match: {title: 'Shawshank'}).types(:movie) EntertainmentIndex.query(match: {author: 'Tarantino'}).only(:id).limit(10).load # the last one loads ActiveRecord objects for documents found
Nyt hakemisto on melkein valmis käytettäväksi hakutoteutuksessamme.
Railsin integroimiseksi tarvitsemme ensin kyvyn reagoida RDBMS-objektimuutoksiin. Chewy tukee tätä käyttäytymistä soittopyyntöjen kautta, jotka on määritelty update_index
luokan menetelmä. update_index
ottaa kaksi argumenttia:
'index_name#type_name'
-sivulla muotoMeidän on määriteltävä nämä takaisinkutsut jokaiselle riippuvaiselle mallille:
class Book Koska tunnisteet on myös indeksoitu, meidän on seuraavaksi apinapattava joitain ulkoisia malleja, jotta ne reagoivat muutoksiin:
ActsAsTaggableOn::Tag.class_eval do has_many :books, through: :taggings, source: :taggable, source_type: 'Book' has_many :videos, through: :taggings, source: :taggable, source_type: 'Video' # Updating all tag-related objects update_index 'entertainment#book', :books update_index('entertainment#movie') { videos.movies } update_index('entertainment#cartoon') { videos.cartoons } end ActsAsTaggableOn::Tagging.class_eval do # Same goes for the intermediate model update_index('entertainment#book') { taggable if taggable_type == 'Book' } update_index('entertainment#movie') { taggable if taggable_type == 'Video' && taggable.movie? } update_index('entertainment#cartoon') { taggable if taggable_type == 'Video' && taggable.cartoon? } end
Tässä vaiheessa jokainen esine Tallentaa tai tuhota päivittää vastaavan Elasticsearch-hakemistotyypin.
Atomisuus
Meillä on edelleen yksi viipyvä ongelma. Jos teemme jotain books.map(&:save)
Jos haluat tallentaa useita kirjoja, pyydämme entertainment
-päivitystä indeksi joka kerta kun yksittäinen kirja tallennetaan . Jos tallennamme viisi kirjaa, päivitämme Chewy-indeksin viisi kertaa. Tämä käyttäytyminen on hyväksyttävää VASTAA , mutta ei varmasti hyväksyttävä ohjaimen toiminnoille, joissa suorituskyky on kriittinen.
Käsittelemme tätä ongelmaa Chewy.atomic
lohko:
class ApplicationController Lyhyesti sanottuna Chewy.atomic
erittää nämä päivitykset seuraavasti:
- Poistaa
after_save
-toiminnon käytöstä soita takaisin. - Kerää tallennettujen kirjojen tunnukset.
Chewy.atomic
: N valmistuttua lohko, käyttää kerättyjä tunnuksia yhden Elasticsearch-hakemiston päivityspyynnön tekemiseen.
Etsitään
Nyt olemme valmiita ottamaan käyttöön hakuliittymän. Koska käyttöliittymä on lomake, paras tapa rakentaa se on tietysti FormBuilder ja ActiveModel . (ApeeScapessa käytämme ActiveData toteuttaa ActiveModel-rajapintoja, mutta voit käyttää suosikkihelmiäsi.)
class EntertainmentSearch include ActiveData::Model attribute :query, type: String attribute :author_id, type: Integer attribute :min_year, type: Integer attribute :max_year, type: Integer attribute :tags, mode: :arrayed, type: String, normalize: ->(value) { value.reject(&:blank?) } # This accessor is for the form. It will have a single text field # for comma-separated tag inputs. def tag_list= value self.tags = value.split(',').map(&:strip) end def tag_list self.tags.join(', ') end end
Kysely ja suodattimet -opetusohjelma
Nyt kun meillä on ActiveModel-tyyppinen objekti, joka voi hyväksyä ja kirjoittaa tyypin määritteitä, toteutetaan haku:
class EntertainmentSearch ... def index EntertainmentIndex end def search # We can merge multiple scopes [query_string, author_id_filter, year_filter, tags_filter].compact.reduce(:merge) end # Using query_string advanced query for the main query input def query_string index.query(query_string: {fields: [:title, :author, :description], query: query, default_operator: 'and'}) if query? end # Simple term filter for author id. `:author_id` is already # typecasted to integer and ignored if empty. def author_id_filter index.filter(term: {author_id: author_id}) if author_id? end # For filtering on years, we will use range filter. # Returns nil if both min_year and max_year are not passed to the model. def year_filter body = {}.tap do |body| body.merge!(gte: min_year) if min_year? body.merge!(lte: max_year) if max_year? end index.filter(range: {year: body}) if body.present? end # Same goes for `author_id_filter`, but `terms` filter used. # Returns nil if no tags passed in. def tags_filter index.filter(terms: {tags: tags}) if tags? end end
Ohjaimet ja näkymät
Tässä vaiheessa mallimme voi suorittaa hakupyyntöjä välitetyillä määritteillä. Käyttö näyttää tältä:
EntertainmentSearch.new(query: 'Tarantino', min_year: 1990).search
Huomaa, että ohjaimeen haluamme ladata tarkat ActiveRecord-objektit sijaan Sitkeä asiakirjan kääreet:
class EntertainmentController Nyt on aika kirjoittaa joitain HAML klo entertainment/index.html.haml
:
= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| = f.text_field :query = f.select :author_id, Dude.all.map d, include_blank: true = f.text_field :min_year = f.text_field :max_year = f.text_field :tag_list = f.submit - if @entertainments.any? %dl - @entertainments.each do |entertainment| %dt %h1= entertainment.title %strong= entertainment.class %dd %p= entertainment.year %p= entertainment.description %p= entertainment.tag_list = paginate @entertainments - else Nothing to see here
Lajittelu
Bonuksena lisäämme myös lajittelun hakutoimintoihimme.
Oletetaan, että meidän on lajiteltava otsikko- ja vuosikentät sekä osuvuuden mukaan. Valitettavasti otsikko One Flew Over the Cuckoo's Nest
jaetaan yksittäisiin termeihin, joten lajittelu näiden erilaisten termien mukaan on liian satunnaista; sen sijaan haluaisimme lajitella koko otsikon mukaan.
Ratkaisu on käyttää erityistä otsikkokenttää ja käyttää omaa analysaattoriaan:
class EntertainmentIndex Lisäksi aiomme lisätä molemmat uudet määritteet ja lajittelun käsittelyvaiheen hakumalliin:
class EntertainmentSearch # we are going to use `title.sorted` field for sort SORT = {title: {'title.sorted' => :asc}, year: {year: :desc}, relevance: :_score} ... attribute :sort, type: String, enum: %w(title year relevance), default_blank: 'relevance' ... def search # we have added `sorting` scope to merge list [query_string, author_id_filter, year_filter, tags_filter, sorting].compact.reduce(:merge) end def sorting # We have one of the 3 possible values in `sort` attribute # and `SORT` mapping returns actual sorting expression index.order(SORT[sort.to_sym]) end end
Lopuksi muokkaamme lomakettamme lisäämällä lajitteluvaihtoehtojen valintaruutu:
= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| ... / `EntertainmentSearch.sort_values` will just return / enum option content from the sort attribute definition. = f.select :sort, EntertainmentSearch.sort_values ...
Virheiden käsittely
Jos käyttäjät suorittavat virheellisiä kyselyitä, kuten (
tai AND
, Elasticsearch-asiakasohjelma aiheuttaa virheen. Tehdään tämä tekemällä joitain muutoksia ohjaimeemme:
class EntertainmentController e @entertainments = [] @error = e.message.match(/QueryParsingException[([^;]+)]/).try(:[], 1) end end
Lisäksi meidän on tehtävä virhe näkymässä:
... - if @entertainments.any? ... - else - if @error = @error - else Nothing to see here
Elasticsearch-kyselyjen testaaminen
Testauksen perusasetukset ovat seuraavat:
- Käynnistä Elasticsearch-palvelin.
- Siivoa ja luoda indeksit.
- Tuo tietomme.
- Suorita kysely.
- Vertaile tulosta odotuksiimme.
Vaiheessa 1 on kätevää käyttää elasticsearch-laajennukset helmi. Lisää vain seuraava rivi projektiisi Rakefile
gem-asennus:
require 'elasticsearch/extensions/test/cluster/tasks'
Sitten saat seuraavan Rake tehtävät:
$ rake -T elasticsearch rake elasticsearch:start # Start Elasticsearch cluster for tests rake elasticsearch:stop # Stop Elasticsearch cluster for tests
Elasticsearch ja Rspec
Ensinnäkin meidän on varmistettava, että hakemistomme päivitetään synkronoitavaksi datamuutosten kanssa. Onneksi Chewy-helmen mukana tulee hyödyllinen update_index
rspec Ottelut:
describe EntertainmentIndex do # No need to cleanup Elasticsearch as requests are # stubbed in case of `update_index` matcher usage. describe 'Tag' do # We create several books with the same tag let(:books) { create_list :book, 2, tag_list: 'tag1' } specify do # We expect that after modifying the tag name... expect do ActsAsTaggableOn::Tag.where(name: 'tag1').update_attributes(name: 'tag2') # ... the corresponding type will be updated with previously-created books. end.to update_index('entertainment#book').and_reindex(books, with: {tags: ['tag2']}) end end end
Seuraavaksi meidän on testattava, että todelliset hakukyselyt suoritetaan oikein ja että ne palauttavat odotetut tulokset:
describe EntertainmentSearch do # Just defining helpers for simplifying testing def search attributes = {} EntertainmentSearch.new(attributes).search end # Import helper as well def import *args # We are using `import!` here to be sure all the objects are imported # correctly before examples run. EntertainmentIndex.import! *args end # Deletes and recreates index before every example before { EntertainmentIndex.purge! } describe '#min_year, #max_year' do let(:book) { create(:book, year: 1925) } let(:movie) { create(:movie, year: 1970) } let(:cartoon) { create(:cartoon, year: 1995) } before { import book: book, movie: movie, cartoon: cartoon } # NOTE: The sample code below provides a clear usage example but is not # optimized code. Something along the following lines would perform better: # `specify { search(min_year: 1970).map(&:id).map(&:to_i) # .should =~ [movie, cartoon].map(&:id) }` specify { search(min_year: 1970).load.should =~ [movie, cartoon] } specify { search(max_year: 1980).load.should =~ [book, movie] } specify { search(min_year: 1970, max_year: 1980).load.should == [movie] } specify { search(min_year: 1980, max_year: 1970).should == [] } end end
Testaa klusterin vianmääritys
Lopuksi tässä on opas testiryhmän vianmääritykseen:
-
Aloita käyttämällä muistin sisäistä yhden solmun ryhmää. Se on paljon nopeampi teknisille tiedoille. Meidän tapauksessamme: TEST_CLUSTER_NODES=1 rake elasticsearch:start
-
elasticsearch-extensions
: Lla on joitain ongelmia itse klusterin toteutus liittyy yhden solmun klusterin tilan tarkistamiseen (se on joissakin tapauksissa keltainen ja ei koskaan vihreä, joten vihreän tilan klusterin käynnistystarkastus epäonnistuu joka kerta). Ongelma on korjattu haarukalla, mutta toivottavasti se korjataan pian päärekisteriin.
-
Ryhmittele pyyntösi jokaiselle tietojoukolle spesifikaatioihin (eli tuo tietosi kerran ja suorita sitten useita pyyntöjä). Elasticsearch lämpenee pitkään ja käyttää paljon kasaa muistia tuotaessa tietoja, joten älä liioittele sitä, varsinkin jos sinulla on joukko teknisiä tietoja.
-
Varmista, että koneellasi on riittävästi muistia, tai Elasticsearch jäädytetään (tarvitsimme noin 5 Gt jokaiselle testaavalle virtuaalikoneelle ja noin 1 Gt itse Elasticsearchille).
Käärimistä
Elasticsearch on itse kuvattu 'joustavaksi ja tehokkaaksi avoimen lähdekoodin, hajautetuksi, reaaliaikaiseksi haku- ja analyysimoottoriksi'. Se on hakutekniikan kultastandardi.
Chewyn kanssa, meidän kiskojen kehittäjät ovat pakanneet nämä edut yksinkertaiseksi, helppokäyttöiseksi, laadukkaaksi, avoimen lähdekoodin Ruby-helmeksi, joka tarjoaa tiukan integraation Railsin kanssa. Elasticsearch and Rails - mikä mahtava yhdistelmä!
Elasticsearch and Rails - mikä mahtava yhdistelmä! Tweet
Liite: Elasticsearch-sisäosat
Tässä on erittäin lyhyt esittely Elasticsearchin 'konepellin alla'…
Elasticsearch on rakennettu Lucene , jota itse käytetään käänteiset indeksit ensisijaisena tietorakenteena. Esimerkiksi jos meillä on merkkijonot 'koirat hyppää korkealle', 'hyppää aidan yli' ja 'aita oli liian korkea', saamme seuraavan rakenteen:
'the' [0, 0], [1, 2], [2, 0] 'dogs' [0, 1] 'jump' [0, 2], [1, 0] 'high' [0, 3], [2, 4] 'over' [1, 1] 'fence' [1, 3], [2, 1] 'was' [2, 2] 'too' [2, 3]
Siten jokainen termi sisältää sekä viittauksia tekstiin että paikkoja siinä. Lisäksi päätämme muuttaa ehtojamme (esim. Poistamalla pysäytyssanat kuten 'the') ja soveltamaan foneettinen hajautus jokaiselle termille (voitko arvata algoritmi ?):
'DAG' [0, 1] 'JANP' [0, 2], [1, 0] 'HAG' [0, 3], [2, 4] 'OVAR' [1, 1] 'FANC' [1, 3], [2, 1] 'W' [2, 2] 'T' [2, 3]
Jos kysytään sitten 'koira hyppää', se analysoidaan samalla tavalla kuin lähdeteksti, josta tulee hajautuksen jälkeen 'DAG JANP' ('koiralla' on sama hash kuin 'koirilla', kuten on totta 'hyppyjen' ja 'hypätä').
Lisäämme myös jonkin verran logiikkaa merkkijonon yksittäisten sanojen väliin (kokoonpanoasetusten perusteella) valitsemalla joko ('DAG' JA 'JANP') tai ('DAG' TAI 'JANP'). Edellinen palauttaa [0] & [0, 1]
: n leikkauspisteen (eli asiakirja 0) ja jälkimmäinen, [0] | [0, 1]
(eli asiakirjat 0 ja 1). Tekstin sisäisiä sijainteja voidaan käyttää tulosten ja sijainnista riippuvien kyselyjen pisteyttämiseen.