具有1对n个实体的模型
用于与Elasticsearch进行交互的模型具有1到n的关系。 SkillWithListOfDetails
类具有SkillDetail
对象的列表。 这些类将作为嵌套对象使用SkillDetail
列表保存到Elasticsearch。 这个子对象可以像父对象SkillWithListOfDetails
中的任何其他属性一样进行搜索。
public class SkillWithListOfDetails
{
[Required]
[Range(1, long.MaxValue)]
public long Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Description { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
public List<SkillDetail> SkillDetails { get; set; }
}
SkillDetail
用作子类。 父外键不需要Id,因为当存储到Elasticsearch时,该子被NESTED。
public class SkillDetail
{
[Required]
[Range(1, long.MaxValue)]
public long Id { get; set; }
[Required]
public string SkillLevel { get; set; }
[Required]
public string Details { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}
控制器创建,使用ElasticsearchCRUD创建
创建Elasticsearch功能使用EleashseachCRUD实现。 要使用NuGet下载ElasticsearchCRUD:
这将使用默认的IElasticSearchMappingResolver
来保存索引为多元状态,将类型设置为不带命名空间的类名称,并将所有属性保存为小写。
一个id是必需的,不是自动生成的。 ElasticseachCRUD不支持自动生成的ID。 通常,Elasticsearch不是主要的持久性,而是将已有ID的文档保存到搜索引擎中的实体。 如果需要创建Id,您可以自动生成一个新的随机Guid。
private const string ConnectionString = "http://localhost:9200/";
private readonly IElasticsearchMappingResolver _elasticsearchMappingResolver = new ElasticsearchMappingResolver();
public void AddUpdateEntity(SkillWithListOfDetails skillWithListOfDetails)
{
using (var context = new ElasticsearchContext(ConnectionString, _elasticsearchMappingResolver))
{
context.AddUpdateDocument(skillWithListOfDetails, skillWithListOfDetails.Id);
context.SaveChanges();
}
}
然后,提供者可以在SearchController
中使用。 action方法接受模型和包含SkillDetail
实体的子列表的字符串。 此createSKillDetailsList
字符串属性是使用javascript
和jTable
从视图创建的json字符串。
[HttpPost]
[Route("Index")]
public ActionResult Index(SkillWithListOfDetails model, string createSkillDetailsList)
{
if (ModelState.IsValid)
{
model.Created = DateTime.UtcNow;
model.Updated = DateTime.UtcNow;
model.SkillDetails =
JsonConvert.DeserializeObject(createSkillDetailsList, typeof(List<SkillDetail>)) as List<SkillDetail>;
_searchProvider.AddUpdateDocument(model);
return Redirect("Search/Index");
}
return View("Index", model);
}
创建视图是一个简单的MVC razor 局部视图。 该视图使用SkillWithListOfDetails
模型,并向MVC控制器动作发送一个简单的表单。 输入按钮调用一个javascript函数,它从jTable创建表格中获取所有SkillDetail
行,并将其添加到输入隐藏项。 然后它执行submti()
@model WebSearchWithElasticsearchNestedDocuments.Search.SkillWithListOfDetails
<div id="createForm">
@using (Html.BeginForm("Index", "Search"))
{
@Html.ValidationSummary(true)
<fieldset class="form">
<legend>CREATE a new document in the search engine</legend>
<table width="800">
<tr>
<th></th>
<th></th>
</tr>
<tr>
<td>
@Html.Label("Id:")
</td>
<td>
@Html.EditorFor(model => model.Id)
@Html.ValidationMessageFor(model => model.Id)
</td>
</tr>
<tr>
<td>
@Html.Label("Name:")
</td>
<td>
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</td>
</tr>
<tr>
<td>
@Html.Label("Description:")
</td>
<td>
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</td>
</tr>
<tr>
<td colspan="2">
<div id="createtableskilldetails" />
<input id="createSkillDetailsList" name="createSkillDetailsList" type="hidden" />
</td>
</tr>
<tr>
<td>
<br />
<input type="button" onclick="SumbitCreateForm()" value="Add Skill" style="width:200px" />
</td>
<td></td>
</tr>
</table>
</fieldset>
}
</div>
SearchCreate
是一个MVC PartialView。 这在Index View中使用。 Index View包含所有的JavaScript实现。 这应该在单独的js文件中实现并捆绑。 JavaScript代码使用3个js库,moment.js
和jTable
和jQuery
(带UI)。
@model WebSearchWithElasticsearchNestedDocuments.Search.SkillWithListOfDetails
<fieldset class="form">
<legend>SEARCH for a document in the search engine</legend>
<table width="500">
<tr>
<th></th>
</tr>
<tr>
<td>
<label for="autocomplete">Search: </label>
</td>
</tr>
<tr>
<td>
<input id="autocomplete" type="text" style="width:500px" />
</td>
</tr>
</table>
</fieldset>
@section scripts
{
<link href="~/Scripts/jtable/themes/jqueryui/jtable_jqueryui.min.css" rel="stylesheet" />
<script src="~/Scripts/jtable/jquery.jtable.min.js"></script>
<script src="~/Scripts/moment.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var updateResults = [];
$("input#autocomplete").autocomplete({
source: function(request, response) {
$.ajax({
url: "http://localhost:50227/Search/search",
dataType: "json",
data: {
term: request.term,
},
success: function(data) {
var itemArray = new Array();
for (i = 0; i < data.length; i++) {
itemArray[i] = { label: data[i].Name, value: data[i].Name, data: data[i] }
}
console.log(itemArray);
response(itemArray);
},
error: function(data, type) {
console.log(type);
}
});
},
select: function(event, ui) {
$("#spanupdateId").text(ui.item.data.Id);
$("#spanupdateCreated").text(moment(ui.item.data.Created).format('DD/MM/YYYY HH:mm:ss'));
$("#spanupdateUpdated").text(moment(ui.item.data.Updated).format('DD/MM/YYYY HH:mm:ss'));
$("#updateName").text(ui.item.data.Name);
$("#updateDescription").text(ui.item.data.Description);
$("#updateName").val(ui.item.data.Name);
$("#updateDescription").val(ui.item.data.Description);
if (ui.item.data.SkillDetails) {
updateResults = ui.item.data.SkillDetails;
}
$('#updatetableskilldetails').jtable('load');
$("#updateId").val(ui.item.data.Id);
$("#updateCreated").val(ui.item.data.Created);
$("#updateUpdated").val(ui.item.data.Updated);
$("#spandeleteId").text(ui.item.data.Id);
$("#deleteId").val(ui.item.data.Id);
$("#deleteName").text(ui.item.data.Name);
console.log(ui.item);
}
});
$('#updatetableskilldetails').jtable({
title: 'Skill Details',
paging: false,
pageSize: 5,
sorting: true,
multiSorting: true,
defaultSorting: 'Name asc',
actions: {
listAction: function (postData) {
console.log("Loading from custom function...");
return {
"Result": "OK",
"Records": updateResults
};
},
deleteAction: function (postData) {
console.log("delete action called for:" + JSON.stringify(postData));
return {
"Result": "OK"
};
},
createAction: function (postData) {
var data = getQueryParams(postData);
return {
"Result": "OK",
"Record": { "Id": data["Id"], "SkillLevel": data["SkillLevel"], "Details": data["Details"], "Created": data["Created"], "Updated": moment() }
}
},
updateAction: function (postData) {
return {
"Result": "OK",
};
}
},
fields: {
Id: {
key: true,
create: true,
edit: true,
list: true
},
SkillLevel: {
title: 'SkillLevel',
width: '20%'
},
Details: {
title: 'Details',
width: '30%'
},
Created: {
title: 'Created',
edit: false,
create: false,
width: '20%',
display: function (data) { return moment(data.record.Created).format('DD/MM/YYYY HH:mm:ss'); }
},
Updated: {
title: 'Updated',
edit: false,
create: false,
width: '20%',
display: function (data) { return moment(data.record.Updated).format('DD/MM/YYYY HH:mm:ss'); }
}
}
});
$('#createtableskilldetails').jtable({
title: 'Skill Details',
paging: false,
pageSize: 5,
sorting: true,
multiSorting: true,
defaultSorting: 'Name asc',
actions: {
deleteAction: function (postData) {
console.log("delete action called for:" + JSON.stringify(postData));
return {
"Result": "OK"
};
},
createAction: function(postData) {
var data = getQueryParams(postData);
return {
"Result": "OK",
"Record": { "Id": data["Id"], "SkillLevel": data["SkillLevel"], "Details": data["Details"], "Created": moment(), "Updated": moment() }
}
},
updateAction: function(postData) {
return {
"Result": "OK",
};
}
},
fields: {
Id: {
key: true,
create: true,
edit: true,
list: true
},
SkillLevel: {
title: 'SkillLevel',
width: '20%'
},
Details: {
title: 'Details',
width: '30%'
},
Created: {
title: 'Created',
edit: false,
create: false,
width: '20%',
display: function(data) { return moment(data.record.Created).format('DD/MM/YYYY HH:mm:ss'); }
},
Updated: {
title: 'Updated',
edit: false,
create: false,
width: '20%',
display: function(data) { return moment(data.record.Updated).format('DD/MM/YYYY HH:mm:ss'); }
}
}
});
}); // End of document ready
function getQueryParams(qs) {
qs = qs.split("+").join(" ");
var params = {},
tokens,
re = /[?&]?([^=]+)=([^&]*)/g;
while (tokens = re.exec(qs)) {
params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
}
return params;
}
function getAllRowsOfjTableUpdateSkillDetailsList() {
var $rows = $('#updatetableskilldetails').find('.jtable-data-row');
var headers = ["Id", "SkillLevel", "Details", "Created", "Updated"];
var data = [];
$.each($rows, function() {
var rowData = {};
for (var j = 0; j < 5; j++) {
console.log(headers[j] + ":" + this.cells[j].innerHTML);
rowData[headers[j]] = this.cells[j].innerHTML;
}
data.push(rowData);
});
$("#updateSkillDetailsList").val(JSON.stringify(data));
}
function getAllRowsOfjTableCreateSkillDetailsList() {
var $rows = $('#createtableskilldetails').find('.jtable-data-row');
var headers = ["Id", "SkillLevel", "Details", "Created", "Updated"];
var data = [];
$.each($rows, function () {
var rowData = {};
for (var j = 0; j < 5; j++) {
console.log(headers[j] + ":" + this.cells[j].innerHTML);
rowData[headers[j]] = this.cells[j].innerHTML;
}
data.push(rowData);
});
$("#createSkillDetailsList").val(JSON.stringify(data));
}
function SumbitUpdateForm() {
getAllRowsOfjTableUpdateSkillDetailsList();
$("#updateForm form").submit();
}
function SumbitCreateForm() {
getAllRowsOfjTableCreateSkillDetailsList();
$("#createForm form").submit();
}
</script>
}
@Html.Partial("SearchUpdate")
@Html.Partial("SearchDelete")
@Html.Partial("SearchCreate")
现在可以创建具有嵌套对象数组的新的Elastissearch文档。视图看起来像这样:
Elasticsearch 索引和映射
当您检查Elasticsearch搜索引擎中的映射时,您将找到具有嵌套数组子项的新文档。
http://localhost:9200//_mapping
{
"skillwithlistofdetailss": {
"mappings": {
"skillwithlistofdetails": {
"properties": {
"created": { "type": "date", "format": "dateOptionalTime" },
"description": { "type": "string" },
"id": { "type": "long" },
"name": { "type": "string" },
"skilldetails": { "properties": { "created": { "type": "date", "format": "dateOptionalTime" }, "details": { "type": "string" }, "id": { "type": "long" }, "skilllevel": { "type": "string" }, "updated": { "type": "date", "format": "dateOptionalTime" } } },
"updated": { "type": "date", "format": "dateOptionalTime" } }
}
}
}
}
使用查询字符串搜索进行搜索
搜索是使用包含查询字符串搜索的搜索类构建的。 此查询使用可用于自动完成的通配符。 这可以通过使用不同的查询类型进行优化。
该term在每个结尾处被分成具有*通配符的不同搜索词。 搜索也搜索嵌套数组。
private static readonly Uri Node = new Uri(ConnectionString);
public IEnumerable<SkillWithListOfDetails> QueryString(string term)
{
var names = "";
if (term != null)
{
names = term.Replace("+", " OR *");
}
var search = new ElasticsearchCRUD.Model.SearchModel.Search
{
From= 0,
Size = 10,
Query = new Query(new QueryStringQuery(names + "*"))
};
IEnumerable<SkillWithListOfDetails> results;
using (var context = new ElasticsearchContext(ConnectionString, _elasticSearchMappingResolver))
{
results = context.Search<SkillWithListOfDetails>(search).PayloadResult.Hits.HitsResult.Select(t => t.Source);
}
return results;
}
然后在SearchController
中使用搜索。 这将使用直接从autocomplete 控件使用的Json数组返回集合。
$[Route("Search")]
public JsonResult Search(string term)
{
return Json(_searchProvider.QueryString(term), "SkillWithListOfDetails", JsonRequestBehavior.AllowGet);
}
查看jTable的autocomplete
autocomplete 控件使用此Json结果,并允许用户选择单个文档。 当选择一个文档时,它将显示在更新控件中。
<input id="autocomplete" type="text" style="width:500px" />
$("input#autocomplete").autocomplete({
source: function(request, response) {
$.ajax({
url: "http://localhost:50227/Search/search",
dataType: "json",
data: {
term: request.term,
},
success: function(data) {
var itemArray = new Array();
for (i = 0; i < data.length; i++) {
itemArray[i] = { label: data[i].Name, value: data[i].Name, data: data[i] }
}
console.log(itemArray);
response(itemArray);
},
error: function(data, type) {
console.log(type);
}
});
},
select: function(event, ui) {
$("#spanupdateId").text(ui.item.data.Id);
$("#spanupdateCreated").text(moment(ui.item.data.Created).format('DD/MM/YYYY HH:mm:ss'));
$("#spanupdateUpdated").text(moment(ui.item.data.Updated).format('DD/MM/YYYY HH:mm:ss'));
$("#updateName").text(ui.item.data.Name);
$("#updateDescription").text(ui.item.data.Description);
$("#updateName").val(ui.item.data.Name);
$("#updateDescription").val(ui.item.data.Description);
if (ui.item.data.SkillDetails) {
updateResults = ui.item.data.SkillDetails;
}
$('#updatetableskilldetails').jtable('load');
$("#updateId").val(ui.item.data.Id);
$("#updateCreated").val(ui.item.data.Created);
$("#updateUpdated").val(ui.item.data.Updated);
$("#spandeleteId").text(ui.item.data.Id);
$("#deleteId").val(ui.item.data.Id);
$("#deleteName").text(ui.item.data.Name);
console.log(ui.item);
}
});
更新控件显示父对象和子对象。类Skilldetails
的子列表显示在jTable JavaScript组件中。
moment.js的DateTime
moment.js库用于以可读格式显示Json DateTime项。 然后在jTable和输入表单中使用这些项。
这个包可以使用NuGet(Moment.js)下载。 使用方法如下:
moment(ui.item.data.Created).format('DD/MM/YYYY HH:mm:ss')
使用ElasticsearchCRUD进行更新
更新方法从视图接收数据,并更新所有更新的时间戳。 类SkillDetail的子列表被添加到实体,然后在Elasticsearch中更新。
public void UpdateSkill(long updateId, string updateName, string updateDescription, List<SkillDetail> updateSkillDetailsList)
{
using (var context = new ElasticsearchContext(ConnectionString, _elasticsearchMappingResolver))
{
var skill = context.GetDocument<SkillWithListOfDetails>(updateId);
skill.Updated = DateTime.UtcNow;
skill.Name = updateName;
skill.Description = updateDescription;
skill.SkillDetails = updateSkillDetailsList;
foreach (var item in skill.SkillDetails)
{
item.Updated = DateTime.UtcNow;
}
context.AddUpdateDocument(skill, skill.Id);
context.SaveChanges();
}
}
delete方法使用_id
字段删除文档。
ElasticsearchCRUD进行删除
public void DeleteSkill(long deleteId)
{
using (var context = new ElasticsearchContext(ConnectionString, _elasticsearchMappingResolver))
{
context.DeleteDocument<SkillWithListOfDetails>(deleteId);
context.SaveChanges();
}
}
结论
使用ElasticsearchCRUD,可以轻松添加,更新,删除1到n个关系的文档。 子元素嵌套在父文档中。 支持集合或对象数组以及简单类型集合/数组。 使用Elasticsearch与ElasticsearchCRUD,您可以创建复杂的搜索查询。