Elasticsearch pakub võimsa, RESTful HTTP-liidese andmete indekseerimiseks ja päringuteks, mis on üles ehitatud Apache Lucene raamatukogu. Kohe karbist välja pakub see skaleeritavat, tõhusat ja tugevat otsingut koos UTF-8 toega. See on võimas tööriist suure hulga struktureeritud andmete indekseerimiseks ja päringute tegemiseks ning siin Ahvi põgenemine , see toetab meie platvormi otsingut ja seda kasutatakse varsti ka automaatse täitmise jaoks. Oleme tohutud fännid.
Chewy laiendab Elasticsearch-Ruby klienti, muutes selle võimsamaks ja pakkudes tihedamat integreerimist Railsiga.Kuna meie platvorm on üles ehitatud Rubiin rööbastel , kasutab meie Elasticsearchi integreerimine järgmist elasticsearch-ruby projekt (Ruby integratsiooniraamistik Elasticsearchile, mis pakub klienti ühenduse loomiseks Elasticsearchi klastriga, Ruby API Elasticsearchi REST API jaoks ning mitmesugused laiendused ja utiliidid). Sellele vundamendile tuginedes oleme välja töötanud ja välja andnud oma paranduse (ja lihtsustuse) Elasticsearchi rakenduse otsinguarhitektuuris, mis on pakitud meie poolt nimetatud rubiinkiviks Nätske (saadaval on näidisrakendus siin ).
Chewy laiendab Elasticsearch-Ruby klienti, muutes selle võimsamaks ja pakkudes tihedamat integreerimist Railsiga. Selles Elasticsearchi juhendis arutlen (kasutusnäidete kaudu), kuidas me selle saavutasime, sealhulgas rakendamise käigus tekkinud tehnilised takistused.
kui palju on ilutööstus väärt 2018. aastat
Enne juhendiga jätkamist piisab vaid paarist kiirest märkmest:
Vaatamata Elasticsearchi mastaapsusele ja efektiivsusele ei osutunud selle integreerimine Railsiga nii lihtsaks, kui arvati. ApeeScape'is leidsime, et peame Elasticsearch-Ruby põhiklienti oluliselt täiendama, et muuta see toimivamaks ja toetada täiendavaid toiminguid.
Vaatamata Elasticsearchi mastaapsusele ja efektiivsusele ei osutunud selle integreerimine Railsiga nii lihtsaks, kui arvati.Ja nii sündiski nätsu pärl.
Chewy mõned eriti tähelepanuväärsed omadused on:
Iga indeks on jälgitav kõigi seotud mudelite järgi.
Enamik indekseeritud mudeleid on omavahel seotud. Mõnikord on vaja need seotud andmed denormaliseerida ja siduda sama objektiga (nt kui soovite märgendite massiivi koos nendega seotud artikliga indekseerida). Chewy võimaldab teil määrata iga mudeli jaoks värskendatava registri, nii et vastavad artiklid lisatakse uuesti iga kord, kui asjakohast silti värskendatakse.
Indeksiklassid ei sõltu ORM / ODM mudelitest.
Selle täiustusega on näiteks mudelitevahelise automaatse täitmise rakendamine palju lihtsam. Saate lihtsalt määratleda indeksi ja töötada sellega objektorienteeritud viisil. Erinevalt teistest klientidest eemaldab Chewy gem vajaduse käsitsi juurutada indeksiklassid, andmete impordi tagasihelistamised ja muud komponendid.
Hulgimport on kõikjal .
Chewy kasutab täieliku reindeksimise ja registrivärskenduste jaoks hulgi Elasticsearch API-d. Samuti kasutab see aatomiuuenduste kontseptsiooni, kogudes muutunud objektid aatomiplokis ja värskendades neid kõiki korraga.
Chewy pakub AR-stiilis päringut DSL.
Olles aheldatav, ühendatav ja laisk, võimaldab see täiustus päringuid tõhusamalt koostada.
OK, nii et vaatame, kuidas see kõik pärlil läbi mängib ...
Elasticsearchil on mitu dokumendiga seotud mõistet. Esimene on index
(a database
analoog in RDBMS ), mis koosneb documents
komplektist, mis võib olla mitu types
(kus type
on omamoodi RDBMS-tabel).
Igas dokumendis on komplekt fields
. Iga välja analüüsitakse iseseisvalt ja selle analüüsivõimalused salvestatakse kausta mapping
selle tüübi järgi. Chewy kasutab seda struktuuri 'sellisena nagu see on' oma objektimudelis:
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
Eespool määratlesime Elasticsearchi indeksi nimega entertainment
kolme tüüpi: book
, movie
ja cartoon
. Iga tüübi jaoks määrasime kogu indeksi jaoks mõned väljade vastendused ja sätete räsi.
Niisiis oleme määratlenud EntertainmentIndex
ja me tahame täita mõned päringud. Esimese sammuna peame looma registri ja importima oma andmed:
EntertainmentIndex.create! EntertainmentIndex.import # EntertainmentIndex.reset! (which includes deletion, # creation, and import) could be used instead
.import
meetod on teadlik imporditud andmetest, kuna oma tüüpide määratlemisel edastasime selle ulatusega; seega impordib see kõik püsiladustuses olevad raamatud, filmid ja koomiksid.
Kui see on tehtud, saame esitada mõned päringud:
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
Nüüd on meie register peaaegu valmis kasutamiseks otsingu rakendamisel.
Railsiga integreerimiseks on esimene asi, mida me oskame reageerida RDBMS-i objekti muudatustele. Chewy toetab seda käitumist tagasihelistamise kaudu, mis on määratletud update_index
klassi meetod. update_index
võtab kaks argumenti:
'index_name#type_name'
vormingusPeame määratlema need tagasihelistamised iga sõltuva mudeli jaoks:
class Book Kuna sildid on ka indekseeritud, peame järgmiseks mõned välised mudelid ahvile lappima, et need reageeriksid muudatustele:
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
Siinkohal iga objekt salvesta või hävitama värskendab vastavat Elasticsearchi indeksitüüpi.
Aatomilisus
Üks püsiv probleem on meil endiselt. Kui teeme midagi sellist books.map(&:save)
mitme raamatu salvestamiseks palume värskendada entertainment
indeks iga kord, kui üksik raamat salvestatakse . Seega, kui salvestame viis raamatut, värskendame Chewy indeksit viis korda. See käitumine on aktsepteeritav VASTUS , kuid kindlasti pole vastuvõetav kontrolleri toimingute puhul, mille jõudlus on kriitiline.
Me käsitleme seda probleemi Chewy.atomic
-ga plokk:
class ApplicationController Lühidalt öeldes: Chewy.atomic
paketab need värskendused järgmiselt:
- Keelab
after_save
helista tagasi. - Kogub salvestatud raamatute ID-sid.
- Pärast
Chewy.atomic
plokk, kasutab kogutud ID-sid ühe Elasticsearchi indeksi värskendustaotluse tegemiseks.
Otsimine
Nüüd oleme valmis kasutama otsinguliidest. Kuna meie kasutajaliides on vorm, on loomulikult parim viis selle loomiseks FormBuilder ja ActiveModel . (ApeeScape'is kasutame ActiveData ActiveModeli liideste juurutamiseks, kuid kasutage julgelt oma lemmik kalliskivi.)
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
Päringute ja filtrite õpetus
Nüüd, kui meil on ActiveModeli-laadne objekt, mis suudab atribuute aktsepteerida ja tüüpilisendada, rakendame otsingu:
pythoni klassil pole atribuuti
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
Kontrollerid ja vaated
Siinkohal saab meie mudel esitada päringuid edastatud atribuutidega. Kasutamine näeb välja umbes selline:
EntertainmentSearch.new(query: 'Tarantino', min_year: 1990).search
Pange tähele, et kontrollerisse soovime laadida täpsed ActiveRecordi objektid Nätske dokumendipakendid:
class EntertainmentController Nüüd on aeg mõned üles kirjutada HAML kell 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
Sorteerimine
Boonusena lisame oma otsingufunktsioonidele ka sortimise.
Oletame, et peame sortima nii pealkirja ja aasta väljadel kui ka asjakohasuse järgi. Kahjuks on pealkiri One Flew Over the Cuckoo's Nest
jaotatakse üksikuteks terminiteks, nii et sortimine nende erinevate terminite järgi on liiga juhuslik; selle asemel tahaksime sortida kogu pealkirja järgi.
Lahendus on kasutada spetsiaalset pealkirjavälja ja rakendada oma analüsaatorit:
class EntertainmentIndex Lisaks lisame oma otsingumudelile nii need uued atribuudid kui ka sortimise töötlemise etapi:
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
Lõpuks muudame oma vormi, lisades sortimisvalikute valikukasti:
= 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 ...
Vigade käitlemine
Kui teie kasutajad esitavad valesid päringuid, näiteks (
või AND
, põhjustab Elasticsearchi klient tõrke. Selle lahendamiseks tehkem kontrolleris mõned muudatused:
class EntertainmentController e @entertainments = [] @error = e.message.match(/QueryParsingException[([^;]+)]/).try(:[], 1) end end
Lisaks peame vea renderdama vaates:
... - if @entertainments.any? ... - else - if @error = @error - else Nothing to see here
Elasticsearchi päringute testimine
Testimise põhiseadistus on järgmine:
javascripti kujundusmustrite intervjuu küsimused
- Käivitage Elasticsearchi server.
- Koristage ja looge meie indeksid.
- Importige meie andmed.
- Tehke meie päring.
- Võrdlege tulemus meie ootustega.
1. etapi jaoks on mugav kasutada jaotises määratletud testiklastrit elasticsearch-extensions pärl. Lisage lihtsalt järgmine rida oma projekti Rakefile
pärlijärgne install:
require 'elasticsearch/extensions/test/cluster/tasks'
Seejärel saate järgmise Reha ülesanded:
$ rake -T elasticsearch rake elasticsearch:start # Start Elasticsearch cluster for tests rake elasticsearch:stop # Stop Elasticsearch cluster for tests
Elasticsearch ja Rspec
Esiteks peame veenduma, et meie indeksit värskendatakse, et see oleks meie andmete muutustega sünkroonne. Õnneks on Chewy pärl kaasas abivalmis update_index
rspec tikud:
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
Järgmisena peame testima, kas tegelikud otsingupäringud täidetakse õigesti ja kas need tagavad oodatud tulemused:
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
Testige klastri tõrkeotsingut
Lõpuks on siin testklastri tõrkeotsingu juhend:
-
Alustamiseks kasutage mälusisest ühe sõlme klastrit. Spetsifikatsioonide jaoks on see palju kiirem. Meie puhul: TEST_CLUSTER_NODES=1 rake elasticsearch:start
-
elasticsearch-extensions
-Ga on probleeme testige klastri juurutamist ise, mis on seotud ühe sõlmega klastri olekukontrolliga (see on mõnel juhul kollane ja pole kunagi roheline, nii et rohelise olekuga klastri käivitamise kontroll ebaõnnestub iga kord). Küsimus on fikseeritud kahvlis, kuid loodetavasti saab see peagi põhirepois korda.
-
Iga andmekogumi jaoks grupeerige oma taotlus spetsifikatsioonidesse (st importige oma andmed üks kord ja täitke seejärel mitu taotlust). Elasticsearch soojeneb pikka aega ja kasutab andmete importimisel palju kuhjaga mälu, nii et ärge üle pingutage, eriti kui teil on hunnik spetsifikatsioone.
-
Veenduge, et teie masinal on piisavalt mälu, vastasel juhul Elasticsearch hangub (iga virtuaalse masina testimiseks vajame umbes 5 GB ja Elasticsearchi enda jaoks umbes 1 GB).
Pakkimine
Elasticsearchi nimetatakse ise „paindlikuks ja võimsaks avatud lähtekoodiga, hajutatud, reaalajas otsingu- ja analüüsimootoriks”. See on otsingutehnoloogiate kuldstandard.
Chewyga meie rööbaste arendajad on need eelised pakkinud lihtsa, hõlpsasti kasutatava, kvaliteetse ja avatud lähtekoodiga Ruby pärlina, mis tagab Railsiga tiheda integreerimise. Elasticsearch ja Rails - kui vinge kombinatsioon!
Elasticsearch ja Rails - kui vinge kombinatsioon! Piiksuma
Lisa: Elasticsearchi sisemine osa
Siin on a väga Elasticsearchi lühike sissejuhatus 'kapoti all' ...
Elasticsearch on üles ehitatud Lucene , mida ise kasutab ümberpööratud indeksid selle esmase andmestruktuurina. Näiteks kui meil on stringid „koerad hüppavad kõrgele”, „hüppavad üle aia” ja „tara oli liiga kõrge”, saame järgmise struktuuri:
'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]
Seega sisaldab iga termin nii viiteid tekstile kui ka positsioone selles. Lisaks otsustame oma tingimusi muuta (nt eemaldades stopp-sõnad nagu “the”) ja rakendada foneetiline räsimine igale terminile (kas oskate arvata algoritm ?):
'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]
Kui küsime siis sõna 'koer hüppab', analüüsitakse seda samamoodi nagu lähteteksti, muutudes pärast räsimist 'DAG JANPiks' ('koeral' on sama räsi kui 'koertel', nagu ka 'hüpped' ja “Hüpe”).
Lisame stringi üksikute sõnade vahele ka teatud loogika (lähtudes konfiguratsiooniseadetest), valides („DAG“ JA „JANP“) või („DAG“ VÕI „JANP“). Esimene tagastab [0] & [0, 1]
ristmiku (s.t dokument 0) ja viimane [0] | [0, 1]
(st dokumendid 0 ja 1). Tekstisiseseid positsioone saab kasutada tulemuste ja asukohast sõltuvate päringute hindamiseks.