ElasticSearch NEST筆記
1. 什么是ElasticSearch?
ElasticSearch is a powerful open source search and analytics engine that makes data easy to explore.
可以簡單理解成索引加檢索的工具,當然它功能多於此。
ElasticSearch分為服務端與客戶端,服務端提供REST API,客戶端使用REST API。
2.怎么安裝Elastic?
-
安裝JDK(下載地址)
-
安裝ElasticSearch(下載地址)
-
解壓,運行\bin\elasticsearch.bat.
-
瀏覽器輸入http://localhost:9200/,可以看到如圖:

-
安裝成功。
-
-
安裝ElasticSearch – header plugin https://github.com/mobz/elasticsearch-head
文檔中有詳細說明
完成后,在如下圖的地方找到一個html


-
安裝完成~
3. 如何使用NEST客戶端(文檔:http://nest.azurewebsites.net/nest/quick-start.html)
-
連接
-
var node = new Uri("http://localhost:9200/");
-
-
var settings = new ConnectionSettings(
-
node,
-
defaultIndex: " geopoint-tests "
-
);
-
-
var client = new ElasticClient(settings);
-
-
添加索引
-
client.CreateIndex("geopoint-tests");
上面這句代碼是可以不用寫的,因為在調用下面的index方法的時候,如果沒有指定使用哪個index,ElasticSearch會直接使用我們在setting中的defaultIndex,如果沒有,則會自動創建。
-
client.Index(obj);
但是如何你需要使用Mapping來調整索引結構,就會需要CreateIndex這個方法。具體的會在下面的Mapping中提到
-
-
添加數據
-
client.Index(obj)
-
-
搜索
正常來說,搜索的需求一般是我們傳入一個keyword(和需要搜索的field name),返回符合條件的列表,那么搜索就分為全文搜索和單屬性搜索。顧名思義,全文搜索就是用keyword去匹配所有的屬性,單屬性搜索就是只匹配指定的屬性。
-
全文搜索:
-
-
-
keyword = String.Format("*{0}*", keyword);
-
//默認的Operator是Or,當keyword是類似於"One Two"之類的中間有空格的時候,會被當成兩個關鍵詞搜索,然后搜索結果進行or運算
-
//所以我們需要根據需求來調整Operator
-
-
var searchResults = client.Search<T>(s => s
-
.Index(index)
-
.Query(q => q.QueryString(qs => qs.Query(keyword).DefaultOperator(Operator.And)))
-
);
-
-
return searchResults.Documents;
-
-
另外由於ES是分詞搜索,所以當我們要用"One"來搜索完整的單詞"JustOne"的時候,就必須在"One"外面添加**,類似於SQL里面的%keyword%,但是這樣的做法會導致在用完整的單詞來搜索的時候搜索不到結果,所以我們需要使用下面的方式(如果有更好的方法請不吝賜教):
-
-
wholeKeyword = keyword;
-
keyword = String.Format("*{0}*", keyword);
-
QueryContainer query = new QueryStringQuery() { Query = keyword, DefaultOperator = Operator.And };
-
if(!String.IsNullOrEmpty(wholeKeyword)){
-
QueryContainer wholeWordQuery = new QueryStringQuery() { Query = wholeKeyword };
-
query = query || wholeWordQuery;
-
}
-
var searchResults = client.Search<Person>(s => s
-
.Index("zhixiao-application")
-
.Query(query)
-
);
-
-
指定屬性搜索
指定屬性的搜索有兩種:
-
使用term Query
Term是一個被索引的精確值,也就是說Foo, foo, FOO是不相等的,因此
在使用term query的時候要注意,term query在搜索的Field已經被索引的時候,是不支持大寫的。下面為elasticSearch - header測試
所有數據:

大寫搜索:

小寫搜索:

NEST的使用:
-
-
-
var searchResults = client.Search<Person>(s => s
-
.Index("zhixiao-application")
-
.Query(q => q.Term(t => t.OnField(f => f.Lastname == "keyword")))
-
);
或者(效果一樣):
-
QueryContainer termQuery = new TermQuery { Field = "lastname", Value = "keyword" };
-
var searchResults = client.Search<Person>(s => s
-
.Index("zhixiao-application")
-
.Query(termQuery)
- );
-
-
PS:term query的Field是必須的,如果Field為空,會產生下面的錯誤
QueryString query一般用於全文搜索,但是也可以用於單個屬性的搜索(設置DefaultField屬性),queryString query可以不區分大小寫。QueryString還有一個好處就是我們可以搜索一個term中的一部分,例如lastname為"t Boterhuis 1",那么我們可以用"terhuis"搜索到這個數據(雖然需要在外面包上**),在term query里面就做不到,因為ES把每一個屬性的值都分析成一個個單獨的term,提高了搜索的效率。
下面為elasticSearch - header測試:
完整term搜索(大寫):

完整term搜索(小寫):

部分搜索(大寫,不帶**):

部分搜索(大寫,帶**):

部分搜索(小寫,帶**):

多詞語搜索:當我們想搜索類似於:"t Boterhuis 2"這樣的多個單詞構成的keyword(用空格分開),term query是無法查詢的,term query顧名思義就是單詞查詢。不能支持多單詞查詢

QueryString query:

大家可以看到,第三條也被搜索進來了,這是因為ES把"t Boterhuis 2"解析成了三個詞匯"t"" Boterhuis""2"。然后分開搜索,把結果集合並,所以ID為4的記錄也被搜索出來了。那么我們如何讓合起來搜索呢?
-
-
-
string keyword = "t Boterhuis 2";
-
QueryContainer wholeWordQuery = new QueryStringQuery() { Query = keyword, DefaultOperator = Operator.And };
-
var searchResults = client.Search<Person>(s => s
-
.Index("zhixiao-application")
-
.Query(wholeWordQuery)
- );
-
-
QueryString query有一個DefaultOperator的屬性,我們可以將其設置為And,這樣搜索的時候,ES會將幾個term的search結果做and運算。
但是有一個比較大的問題是如果我的keyword是"Aberdeen Boterhuis",
這樣也可以搜索出結果來:

我們搜索出了ID為7的數據,因為這條數據firstname里面有Aberdeen ,last name里面有Boterhuis。
目前還沒有找到比較好的方法來解決這個問題,如果各位有好的方法請不吝賜教。
搜索需要我們重新構建索引,這樣才能發現錯誤並且解決他們。
首先我們把原先的索引先刪除了
-
-
var response = client.DeleteIndex(new DeleteIndexRequest(new IndexNameMarker() { Name = "zhixiao-application", Type = typeof(Person) };
-
然后重新創建索引
-
-
var indexResult = client.CreateIndex("zhixiao- application");
-
var response1 = client.Map<Person>(m => m.MapFromAttributes());
-
IEnumerable<Person> persons = new List<Person>
-
{
-
new Person()
-
{
-
Id = "4",
-
Firstname = "Boterhuis-040",
-
Lastname = "Gusto-040",
-
Chains = new string[]{ "a","b","c" },
-
},
-
new Person()
-
{
-
Id = "5",
-
Firstname = "sales@historichousehotels.com",
-
Lastname = "t Boterhuis 1",
-
Chains = new string[]{ "a","b","c" },
-
},
-
new Person()
-
{
-
Id = "6",
-
Firstname = "Aberdeen #110",
-
Lastname = "sales@historichousehotels.com",
-
Chains = new string[]{ "a","b","c" },
-
},
-
new Person()
-
{
-
Id = "7",
-
Firstname = "Aberdeen #110",
-
Lastname = "t Boterhuis 2",
-
Chains = new string[]{ "a","b","c" },
-
},
-
};
-
foreach (var person in persons)
-
{
-
client.Index(person);
-
}
-
Person的類:
-
-
public class Person
-
{
-
public string Id { get; set; }
-
public string Firstname { get; set; }
-
public string Lastname { get; set; }
-
public string[] Chains { get; set; }
-
}
-
好了,讓我們來測試下排序~
1).ID排序

正常!
.Index("zhixiao-application")
.Sort(sort => sort.OnField(f => f.Id).Order(SortOrder.Ascending))
);

咦,奇怪,數據哪里去了呢?剛才ID的排序正常,為啥這里就不正常了呢?
我們先斷點看下:

可以看到,ConnnectionStatus的status code是200,說明連接成功,但是我們又沒有查詢出數據來,接下來就需要我們的header工具了

在最后一個tab中,header工具允許我們將ConnectionStatus.Request中的json用於查詢,以此來驗證對錯。我們可以看到,查詢的時候報了一個錯誤
Can't sort on string types with more than one value per doc, or more than one token per field
因為我們的數據是
這樣的,在解析的時候,firstname會被analyse成多個term,所以在排序的時候,就會出錯。
那么我們要怎么辦呢?
在這里我找到了一些答案,我們需要將一個我們需要排序的字段mapping成兩個不同的字段,一個經過分析(e.g. firstname),一個沒有經過分析(e.g. firtname.sort ).
在NEST中,有一個簡便的方法,就是在你需要排序的屬性上面加一個Attribute
所以我們先將原先的索引刪除,然后重新添加索引。
client.Map<Person>(m => m.MapFromAttributes());
這句代碼很重要(在調用它之前需要先CreateIndex),它根據ElasticProperty對對象進行了mapping,使得firstname變成了一個multi fields,這點我們可以從 Request的Json中紅色部分看出
{ "person": { "properties": { "id": { "type": "string" }, "firstname": { "type": "multi_field", "fields": { "firstname": { "type": "string" }, "sort": { "index": "not_analyzed", "type": "string" } } }, "lastname": { "type": "string" }, "chains": { "type": "string" } } } }
另外在header中可以發現,屬性中多了一個person.firstname.sort

所以我們現在可以使用這個屬性來進行排序了
代碼如下:
var searchResults = client.Search<Person>(s => s
.Index("zhixiao-application")
.Sort(sort => sort.OnField(f => f.Firstname.Suffix("sort")).Ascending())
);
PS:注意上面紅色部分的代碼

排序結果如下:

成功!
3.距離排序
Elastic Search 自帶了距離的排序和距離的篩選。所以我們只需要將索引建好,就可以使用。
好了,下面我們就一步步來進行:
public class Location
{
public string Name { get; set; }
[ElasticProperty(Type = FieldType.GeoPoint)]
public Coordinate Coordinate { get; set; }
}
public class Coordinate
{
public double Lat { get; set; }
public double Lon { get; set; }
}
PS:[ElasticProperty(Type = FieldType.GeoPoint)]這個attribute是為了下面mapping的時候,ES會將其識別為GeoPoint
client.CreateIndex("geopoint-tests", s => s
.AddMapping<Location>(f => f
.MapFromAttributes()
.Properties(p => p
.GeoPoint(g => g.Name(n => n.Coordinate).IndexLatLon())
)
)
);
下面是創建索引並且mapping的request json:
{ "settings": { "index": { } }, "mappings": { "location": { "properties": { "name": { "type": "string" }, "coordinate": { "type": "geo_point", "lat_lon": true } } } } }
client.IndexMany(new[] { createLocation("1", 52.310551, 5.07039), createLocation("2", 52.310551, 10.761176), createLocation("3", 52.310551, 8.07039), createLocation("4", 52.310551, 6.07039), }); private static Location createLocation(string name, double latitude, double longitude) { return new Location { Name = name, Coordinate = new Coordinate { Lat = latitude, Lon = longitude } }; }
(4)排序的使用:
-
-
var results = client.Search<Location>(s => s
-
.SortGeoDistance(sort => sort.OnField("coordinate").PinTo(52.310551, 4.404954).Ascending()));
-
結果:

成功!
代碼如下:
-
-
var results = client.Search<Location>(s => s
-
.Filter(f => f.GeoDistance("coordinate", fd => fd.Distance(100, GeoUnit.Kilometers).Location(52.310551, 4.404954)))
-
.SortGeoDistance(sort => sort.OnField("coordinate").PinTo(52.310551, 4.404954).Ascending()));
-
結果如下:

由於比較簡單,我就不解釋啥了,直接上代碼
Person newperson = new Person() { Id = "7", Firstname = "Aberdeen #110", Lastname = "Update", Chains = new string[] { "a", "b", "c" }, }; UpdateRequest<Person> updateRequest = new UpdateRequest<Person>(7) { Doc = newperson }; IUpdateResponse updateResponse = updateClient.Update<Person, Person>(updateRequest); if (updateResponse.ConnectionStatus.HttpStatusCode != 200) { updateClient.Index(newperson); } client.ClearCache(); var searchResults = updateClient.Search<Person>(s => s .Index("zhixiao-application") .Query(q => q.Term(t => t.OnField(f => f.Id).Value(7))) ); Console.WriteLine(searchResults.Documents.Count()); foreach (var person in searchResults.Documents) { Console.WriteLine(person.Id + "," + person.Lastname); }
PS:需要注意的是,如果你需要清除一個屬性的值,傳入null會導致ES認為你不需要更新這個屬性,所以清除一個屬性的值我們需要傳入一個""。
updateClient.Delete<Person>(d => d.Id(7));
//delete one by object
updateClient.Delete<Person>(new Person() { });
//delete the Indices
updateClient.DeleteIndex(new DeleteIndexRequest(new IndexNameMarker() { Name = "zhixiao-application", Type = typeof(Person) }));
