原文地址:https://damienbod.com/2014/10/08/mvc-crud-with-elasticsearch-nested-documents/

本文演示了如何使用jquery、jtable和moment.js在MVC Razor视图中创建带有嵌套对象的ElasticSearch文档,并使用MVC、ElasticSearchCrud和ElasticSearch作为后端。SearchController提供了所有CRUD操作,因此您可以使用1到N个实体或ElasticSearch嵌套文档进行实验。并非所有的ElasticSearch客户端都提供了一种使用嵌套对象查看文档的方法。

Code: https://github.com/damienbod/WebSearchWithElasticsearchNestedDocuments (fork: https://github.com/wtujvk/WebSearchWithElasticsearchNestedDocuments

  1.  ElasticsearchCRUD 介绍
  2. 使用自动完成、jQuery和JTALE的简单文档搜索MVC应用程序
  3. 具有嵌套文档的MVC搜索的CRUD
  4. 利用实体框架从MS SQL Server获取数据传输到Elasticsearch
  5. 带有子文档的MVC的搜索
  6. 基于实体框架和搜索的MVC应用
  7. 实时重建Elasticsearch的索引
  8. 基于搜索和Web API导出CSV
  9. 父、子、孙子记录和路由的检索
  10. Elasticsearch的类型映射到ElasticsearchCRUD
  11. 使用搜索语句的搜索同义词分析器
  12. 使用德国分析器搜索
  13. 基于谷歌的MVC地图搜索
  14. 查询和过滤的搜索功能
  15. Elasticsearch批量插入
  16. Elasticsearch聚合搜索
  17. 多个Index和Type的搜索
  18. 搜索高亮
  19. 索引权重

1个实体

用于与ElasticSearch交互的模型具有1到N的关系。类skillWithListOfDetails具有skillDetail对象的列表。这些类将保存到ElasticSearch,其中skillDetail列表作为嵌套对象。此子对象可以像父对象中的任何其他属性一样进行搜索,并使用详细信息列表进行搜索。

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作为子类,不需要父类的外键,当它存储到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; }
}

Controller Create, Create with 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 create表中获取所有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。这在索引视图中使用。索引视图包含所有的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"
          }
        }
      }
    }
  }
}

带参数查询

使用包含查询字符串搜索的搜索类生成搜索。此查询采用可用于自动完成的通配符。这可以通过使用不同的查询类型进行优化。

这个词被分成不同的搜索词,每个词的结尾都有一个*通配符。搜索还搜索嵌套数组。

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中使用搜索。这将以JSON数组的形式返回集合,其中直接从autocomplete控件使用。

$[Route("Search")]
public JsonResult Search(string term)
{
    return Json(_searchProvider.QueryString(term), "SkillWithListOfDetails", JsonRequestBehavior.AllowGet);
}

查看 jTable的自动完成功能

自动完成控件使用此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);
}
});
View Code

更新控件显示父对象和子对象。子技能详细信息列表显示在jtable javascript组件中。

使用 moment.js的时间功能

moment.js库用于以可读格式显示JSON日期时间项。这些项随后在jtable和输入表单中使用。

 

这个包可以使用nuget(moment.js)下载。其用途如下:

moment(ui.item.data.Created).format('DD/MM/YYYY HH:mm:ss')

更新ElasticsearchCRUD

update方法从视图中获取接收到的数据,并更新所有更新的时间戳。子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文件删除文档。

public void DeleteSkill(long deleteId)
{
    using (var context = new ElasticsearchContext(ConnectionString, _elasticsearchMappingResolver))
    {
        context.DeleteDocument<SkillWithListOfDetails>(deleteId);
        context.SaveChanges();
    }
}

结论

使用ElasticSearchCrud,可以非常容易地添加、更新、删除具有1到n关系的文档。子元素嵌套在父文档中。支持对象的集合或数组以及简单类型的集合/数组。使用ElasticSearch和ElasticSearchCrud,可以创建复杂的搜索查询。

链接

https://www.nuget.org/packages/ElasticsearchCRUD/

http://www.elasticsearch.org/blog/introducing-elasticsearch-net-nest-1-0-0-beta1/

http://www.elasticsearch.org/

https://github.com/elasticsearch/elasticsearch-net

http://nest.azurewebsites.net/

http://jqueryui.com/autocomplete/

http://joelabrahamsson.com/extending-aspnet-mvc-music-store-with-elasticsearch/

http://joelabrahamsson.com/elasticsearch-101/

http://www.spacevatican.org/2012/6/3/fun-with-elasticsearch-s-children-and-nested-documents/

http://thomasardal.com/elasticsearch-migrations-with-c-and-nest/

http://momentjs.com/

http://jtable.org/

posted on 2019-01-29 16:23  余昭(Ray)  阅读(142)  评论(0)    收藏  举报