如何在我們的Asp.NET Core應(yīng)用程序中使用ElasticSearch高級(jí)功能
在上一篇文章中[1],我們討論了將ElasticSearch用作簡單的全文本搜索引擎,如何快速安裝和配置它以及如何將其集成到我們的.NET Web應(yīng)用程序中。
今天,我們?nèi)匀灰陔娮由虅?wù)網(wǎng)站中向您展示如何使用ElasticSearch的許多功能來改善搜索。
我們使用了沒有嵌套類的平面Product類來輕松管理搜索,但是這種方法有很多限制。然后,我們引入了一個(gè)新的數(shù)據(jù)模型,以便任何對(duì)象都是要建模的實(shí)體。一個(gè)文檔可以包含無限數(shù)量的相關(guān)字段和值(數(shù)組,簡單和復(fù)雜類型),并保存為JSON文檔。
我們的模型產(chǎn)品類別已變?yōu)椋?/p>
- public class Product
- {
- public int Id { get; set; }
- public string Ean { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- public Brand Brand { get; set; }
- public Category Category { get; set; }
- public Store Store { get; set; }
- public decimal Price { get; set; }
- public string Currency { get; set; }
- public int Quantity { get; set; }
- public float Rating { get; set; }
- public DateTime ReleaseDate { get; set; }
- public string Image { get; set; }
- public List<Review> Reviews { get; set; }
- }
其中品牌,類別,商店評(píng)論和用戶類別分別是:
- public class Brand
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- }
- public class Category
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- }
- public class Store
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- }
- public class Review
- {
- public int Id { get; set; }
- public short Rating { get; set; }
- public string Description { get; set; }
- public User User { get; set; }
- }
- public class User
- {
- public int Id { get; set; }
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public string IPAddress { get; set; }
- public GeoIp GeoIp { get; set; }
- }
GeoIp是NEST庫中用于地理數(shù)據(jù)的類。產(chǎn)品索引已被簡單地命名為產(chǎn)品。我們以這種方式創(chuàng)建和配置了它:
- client.Indices.Create(“products”, index => index
- .Map<Product>(x => x.AutoMap())
- .Map<Brand>(x => x.AutoMap())
- .Map<Category>(x => x.AutoMap())
- .Map<Store>(x => x.AutoMap())
- .Map<Review>(x => x.AutoMap())
- .Map<User>(x => x.AutoMap()
- .Properties(props => props
- .Keyword(t => t.Name("fullname"))
- .Ip(t => t.Name(dv => dv.IPAddress))
- .Object<GeoIp>(t => t.Name(dv => dv.GeoIp))
- )
- )
- )
我們專門為ElasticSearch索引創(chuàng)建一個(gè)名為fullname的新屬性,用于名為fullname的User類,并定義了將要處理的地理信息。 為了使我們的產(chǎn)品能夠在索引之前進(jìn)行處理,一種有用的方法是node.ingest,即進(jìn)行文檔預(yù)處理的節(jié)點(diǎn)。接收節(jié)點(diǎn)攔截所有索引請(qǐng)求,甚至是批量索引請(qǐng)求,并將所有定義的轉(zhuǎn)換應(yīng)用于其內(nèi)容,然后將文檔發(fā)還給索引API。
必須通過以下參數(shù)在配置文件elasticsearch.yml中。
啟用node.ingest:
- node.ingest: true
在我們的示例中,我們使用相同的節(jié)點(diǎn)進(jìn)行搜索和攝取,我們不需要編寫代碼來管理攝取節(jié)點(diǎn),但是,如果我們要擁有一組專用的攝取節(jié)點(diǎn),則必須配置ElasticSearch客戶如下:
- var pool = new StaticConnectionPool(new []
- {
- new Uri("http://ingestnode1:9200"),
- new Uri("http://ingestnode2:9200"),
- new Uri("http://ingestnode3:9200")
- });
- var settings = new ConnectionSettings(pool);
- var client = new ElasticClient(settings);
為了對(duì)文檔進(jìn)行預(yù)處理,需要在建立索引之前定義一個(gè)管道,該管道指定一組能夠轉(zhuǎn)換該文檔的過程。有許多默認(rèn)過程可供使用。例如:GeoIP從IP地址獲取地理信息,JSON將字符串轉(zhuǎn)換為JSON對(duì)象,小寫和大寫,Drop刪除與某些參數(shù)匹配的文檔。您也可以創(chuàng)建自定義過程。我們?cè)陧?xiàng)目中使用的管道是:
- client.Ingest.PutPipeline("product-pipeline", p => p
- .Processors(ps => ps
- .Uppercase<Brand>(s => s
- .Field(t => t.Name)
- )
- .Uppercase<Category>(s => s
- .Field(t => t.Name)
- )
- .Set<User>(s => s.Field("fullname")
- .Value(s.Field(f => f.FirstName) + " " +
- s.Field(f => f.LastName)))
- .GeoIp<User>(s => s
- .Field(i => i.IPAddress)
- .TargetField(i => i.GeoIp)
- )
- )
- );
該管道處理文檔,以便:
- Brand.Name和Category.Name將通過大寫輸入以大寫形式索引;
- User.fullname將包含名字和姓氏(設(shè)置攝取);
- User.IPAddress將成為地理定位的地理地址(GeoIp提取)。
管道以ElasticSearch集群狀態(tài)保存,要使用它們,您必須在索引請(qǐng)求中指定管道參數(shù),以便攝取節(jié)點(diǎn)知道必須使用哪個(gè)管道:
- client.Bulk(b => b
- .Index("products")
- .Pipeline("product-pipeline")
- .Timeout("5m")
- .Index<Person>(/*snip*/)
- .Index<Person>(/*snip*/)
- .Index<Person>(/*snip*/)
- .RequestConfiguration(rc => rc
- .RequestTimeout(TimeSpan.FromMinutes(5))
- )
- );
通過這種方式,我們定義了索引編制過程,以便我們可以根據(jù)需要獲取文檔清單。在使用創(chuàng)建的管道為文檔建立索引之后,我們可以通過使用瀏覽器http:// localhost:9200 / products / _search進(jìn)行訪問來檢查它們。我們得到類似于以下結(jié)果:
如上一篇文章所述,搜索過程基于文檔分析。這是第一個(gè)階段的令牌化過程(將文本分成小塊,稱為令牌),另一個(gè)是規(guī)范化過程(它允許您查找與不等于搜索詞但足夠相似以至于相關(guān)的令牌的匹配項(xiàng))為搜索建立索引的文本。分析儀執(zhí)行此過程。
分析儀由三個(gè)主要部分組成:
1.0個(gè)或多個(gè)字符過濾器
2.1個(gè)分詞器
3.0個(gè)或多個(gè)令牌過濾器
有一些默認(rèn)的分析器可以使用,但是,為了根據(jù)我們的要求提高搜索的準(zhǔn)確性,我們創(chuàng)建了一個(gè)自定義分析器。
定制分析器使我們能夠在分析過程中控制令牌化之前對(duì)文檔的任何更改,如何將其轉(zhuǎn)換為令牌以及如何對(duì)其進(jìn)行規(guī)范化。
這是我們的自定義分析器:
- var an = new CustomAnalyzer();
- an.CharFilter = new List<string>();
- an.CharFilter.Add("html_strip");
- an.Tokenizer = "edgeNGram";
- an.Filter = new List<string>();
- an.Filter.Add("standard");
- an.Filter.Add("lowercase");
- an.Filter.Add("stop");
- settings.Analysis.Tokenizers.Add("edgeNGram", new Nest.EdgeNGramTokenizer
- {
- MaxGram = 15,
- MinGram = 3
- });
- settings.Analysis.Analyzers.Add("product-analyzer", an);
我們的分析器使用標(biāo)準(zhǔn)的標(biāo)記化方法,創(chuàng)建3至15個(gè)字符的小寫標(biāo)記。我們可以將分析器添加到一個(gè)或多個(gè)字段的索引中,也可以將其添加為標(biāo)準(zhǔn)分析器。
- client.CreateIndex("products", c => c
- // Analyzer added only for the property Description of Product
- .AddMapping<Product>(e => e
- .MapFromAttributes()
- .Properties(p => p.String(s => s.Name(f => f.Description)
- .Analyzer("product-analyzer")))
- )
- //Analyzer added as default
- .Analysis(analysis => analysis
- .Analyzers(a => a
- .Add("default", an)
- )
- )
- )
創(chuàng)建自定義分析器時(shí),可以使用測(cè)試API對(duì)其進(jìn)行測(cè)試。即使對(duì)于默認(rèn)分析儀,也可以執(zhí)行這些測(cè)試。
- var analyzeResponse = client.Indices.Analyze(a => a
- .Tokenizer("standard")
- .Filter("lowercase", "stop")
- .Text("Lorem ipsum dolor sit amet, consectetur...")
- );
我們還可以使用數(shù)據(jù)聚合來提供通過搜索查詢聚合的數(shù)據(jù),它基于可以組成以獲得復(fù)雜聚合的簡單塊。聚合有不同類型,每種類型都有定義的范圍和輸出。它們可以分為:
- 桶裝:具有關(guān)鍵和標(biāo)準(zhǔn)的容器;
- 指標(biāo):根據(jù)一組文檔計(jì)算的指標(biāo);
- 矩陣:在不同文檔字段上進(jìn)行的一系列操作,以矩陣樣式生成數(shù)據(jù);
- 管道:更多聚合的聚合。
在我們的案例中,我們使用匯總來獲取品牌,類別,價(jià)格范圍的產(chǎn)品數(shù)量。在以下示例中,我們找到了產(chǎn)品價(jià)格的匯總:
- s => s
- .Query(...)
- .Aggregations(aggs => aggs
- .Average("average_price", avg => avg.Field(p => p.Price))
- .Max("max_price", avg => avg.Field(p => p.Price))
- .Min("min_price", avg => avg.Field(p => p.Price))
- )
另一個(gè)有用的聚合是根據(jù)品牌,商店或類別進(jìn)行分組:
- s => s
- .Query(...)
- .Aggregations(aggs => aggs
- .ValueCount("products_for_category", avg => avg.Field(p => p.Category.Name))
- .ValueCount("products_for_brand", avg => avg.Field(p => p.Brand.Name))
- .ValueCount("products_for_store", avg => avg.Field(p => p.Store.Name))
- )
這樣,我們可以實(shí)時(shí)獲取針對(duì)類別,品牌和商店的搜索產(chǎn)品數(shù)量。匯總數(shù)據(jù)還可以用于創(chuàng)建儀表板,甚至可以使用動(dòng)態(tài)過濾器(類似于電子商務(wù))來組織搜索,并且顯然可以用于統(tǒng)計(jì)目的。
改善您的搜索
如您所知,我們對(duì)任何搜索結(jié)果都有分?jǐn)?shù)。等級(jí)是從0到1的數(shù)字,它確定搜索參數(shù)如何接近該結(jié)果。得分主要取決于三個(gè)參數(shù):搜索詞的頻率,倒排文檔的頻率和字段長度。
要從得分中排除得分過低的人,我們可以使用MinScore:
- s => s
- .MinScore(0.5)
- .Query(...)
這樣,我們可以排除分?jǐn)?shù)低于0.5的所有結(jié)果。建議者允許您使用與搜索文本相似的術(shù)語來搜索ElasticSearch索引。例如,完成建議器對(duì)于自動(dòng)完成很有用,它會(huì)在鍵入文本時(shí)引導(dǎo)您獲得最佳和更相關(guān)的結(jié)果。該完成建議程序經(jīng)過優(yōu)化,可以盡快返回結(jié)果,但是它使用啟用了快速查找的結(jié)構(gòu)并需要資源。
在我們的案例中,我們實(shí)現(xiàn)了基于產(chǎn)品名稱的自動(dòng)完成方法,該方法將在搜索框中鍵入以下內(nèi)容時(shí)被調(diào)用:
- s => s
- .Query(...)
- .Suggest(su => su
- .Completion("name", cs => cs
- .Field(f => f.Name)
- .Fuzzy(f => f
- .Fuzziness(Fuzziness.Auto)
- )
- .Size(5)
- )
- )
更好的搜索的另一有用方法是索引boost。當(dāng)您搜索更多索引時(shí),可以為這些索引分配一個(gè)乘數(shù),這樣一來,一個(gè)索引的結(jié)果將比另一個(gè)顯示更多。您可以將其用于商業(yè)目的,與供應(yīng)商達(dá)成協(xié)議或使我們的產(chǎn)品脫穎而出。索引提升的一個(gè)示例是:
- s => s
- .Query(...)
- .IndicesBoost(b => b
- .Add("products-1", 1.5)
- .Add("products-2", 1)
- )
在此示例中,我們將乘數(shù)1.5乘以1的結(jié)果,乘以1乘以2的結(jié)果,這樣乘積1的結(jié)果將被更頻繁地顯示。改進(jìn)搜索的另一種方法是通過一些參數(shù)對(duì)它們進(jìn)行排序。就我們而言,我們可以:
- s => s
- .Query()
- .Sort(ss => ss
- .Descending(SortSpecialField.Score)
- .Descending(p => p.Price)
- .Descending(p => p.ReleaseDate)
- .Ascending(SortSpecialField.DocumentIndexOrder)
- )
我們將評(píng)分,價(jià)格,發(fā)布日期以及最終索引順序設(shè)置為更高的優(yōu)先級(jí)。
運(yùn)行項(xiàng)目
我們的示例項(xiàng)目是一個(gè).NET Core MVC WebApi應(yīng)用程序,該應(yīng)用程序提供一個(gè)搜索框和一個(gè)儀表板,其中的儀表板會(huì)根據(jù)鍵入的文本自動(dòng)刷新數(shù)據(jù)。首次運(yùn)行項(xiàng)目時(shí),我們可以加載由Bogus插件創(chuàng)建的n個(gè)Product對(duì)象。還有其他偽造類可以為品牌,類別,商店,評(píng)論和用戶構(gòu)建隨機(jī)對(duì)象。它允許您擁有一個(gè)數(shù)據(jù)庫來執(zhí)行我們的搜索。
- var productFaker = new Faker<Product>()
- .CustomInstantiator(f => new Product())
- .RuleFor(p => p.Id, f => f.IndexFaker)
- .RuleFor(p => p.Ean, f => f.Commerce.Ean13())
- .RuleFor(p => p.Name, f => f.Commerce.ProductName())
- .RuleFor(p => p.Description, f => f.Lorem.Sentence(f.Random.Int(5, 20)))
- .RuleFor(p => p.Brand, f => f.PickRandom(brands))
- .RuleFor(p => p.Category, f => f.PickRandom(categories))
- .RuleFor(p => p.Store, f => f.PickRandom(stores))
- .RuleFor(p => p.Price, f => f.Finance.Amount(1, 1000, 2))
- .RuleFor(p => p.Currency, "€")
- .RuleFor(p => p.Quantity, f => f.Random.Int(0, 1000))
- .RuleFor(p => p.Rating, f => f.Random.Float(0, 1))
- .RuleFor(p => p.ReleaseDate, f => f.Date.Past(2))
- .RuleFor(p => p.Image, f => f.Image.PicsumUrl())
- .RuleFor(p => p.Reviews, f => reviewFaker.Generate(f.Random.Int(0, 1000))
- )
在頁面中間,有一個(gè)儀表板,我們?cè)谄渲惺褂昧吮疚慕榻B的過濾器,分析器和方法。在頂部的搜索框中鍵入一些文本時(shí),將建議相關(guān)產(chǎn)品,并且儀表板內(nèi)容將根據(jù)搜索文本進(jìn)行更新。
結(jié)論
在本文中,我向您展示了如何使用Elasticsearch對(duì)復(fù)雜的實(shí)際場(chǎng)景進(jìn)行有效的處理,分析和搜索數(shù)據(jù)。希望我對(duì)這個(gè)話題感興趣。
此處[2]提供了[3]帶有本文中使用的代碼的示例項(xiàng)目。
References
[1] 一篇文章中: https://www.blexin.com/en-US/Article/Blog/How-to-integrate-ElasticSearch-in-ASPNET-Core-70
[2] 此處: https://github.com/enricobencivenga/ProductElasticSearchAdvanced
[3] 了: https://github.com/enricobencivenga/ProductElasticSearchAdvanced