【发布时间】:2017-08-05 04:22:03
【问题描述】:
自从我上次访问我们内置在内部工具中的特定功能以来已经有一段时间了。基本上它是一个简单的input 文本框字段,我们允许人们在其中执行搜索。当人们搜索时,我们会显示一个自动完成功能,其中列出了与用户搜索的内容相匹配的 5 个项目。
在这个项目开始的相当长一段时间里,我相信我们的自动完成功能运行得非常快——只要我们在这个字段中点击三个字符,我们就会立即启动自动完成功能——我们正在使用弹性搜索来索引我们的记录。如今,自动完成功能有效,但需要很长时间才能得到一些结果。很多时候,用户可以输入整个搜索字符串,当他/她在点击此页面之前按 Enter 进入我们的搜索结果页面时,最终会出现自动完成功能。好吧,因为用户已经前往搜索结果页面,所以它出现得太晚了。
我想我会发布我们正在做的事情,看看我们是否缺少某些东西或者它是否与硬件相关(因为我们正在使用虚拟机进行搜索)。无论如何,我们有一个基本的输入文本框:
<input runat="server" spellcheck="false" autocomplete="off" id="search" type="text" placeholder="Search..." class="search" maxlength="100" />
在这个搜索框的条目上,我们调用一个函数来进行这样的搜索:
function DoAJAXSearch() {
var search = $("#search").val().trim();
var resultsToReturn = 5;
var module;
//have they selected a specific module to search on?
if (search.length > 0) {
if ($('#ddlEntity').val() === "") {
module = 0;
} else {
module = $('#<%= ddlEntity.ClientID %> option:selected').val();
}
$("#search").autocomplete({
source: function (request, response) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "/Code/WebServices/Search.asmx/DoSearch",
dataType: "json",
data: '{"searchQuery":"' + search + '","module":"' + module + '","resultsToReturn":"' + resultsToReturn + '"}',
success: function (data) {
response($.map(data.d, function (item) {
return {
entity: item.EntityType,
value: item.Summary,
url: item.EntityLink,
picture: item.PicturePath == null ? "/Images/noimagefound.png" : item.PicturePath
}
}));
}
});
},
minLength: 3,
select: function (event, ui) {
//log the search back to the database
LogSearch(module, search, true);
//end log search back to database
//open the selected item
window.open(ui.item.url, "_self");
},
open: function () {
},
close: function () {
},
error: function (xmlHttpRequest, textStatus) {
alert(textStatus);
},
search: function () {
$(this).addClass('autocompleteloading');
},
response: function () {
$(this).removeClass('autocompleteloading');
}
}).data("ui-autocomplete")._renderItem = function (ul, item) {
ul.addClass("addShadow");
var face;
if (item != null) {
if (item.value.length > 50) {
face = item.value.substr(0, 50) + '...';
} else {
face = item.value;
}
return $("<li>")
.append("<a title='" + item.value + "' href='" + item.url + "'><div style='float:left;width:100%;'><div class='dPicture'><img width='48px' height='48px' src='" + item.picture + "' /></div><div class='dPicture'><div class='dPictureFields'><b>" + item.entity + "</b></div><div class='dPictureFields'>" + face + "</div></div></div><div class='clear-fix'></div></a></li>")
.appendTo(ul);
}
return false;
};
}
}
基本上,正如您在此处看到的,我们允许用户搜索所有模块或特定模块(如果他们选择了一个)。我们只在输入 3 个字符时调用此 Web 方法:“DoSearch”。
DoSearch 是 asmx 文件中的一个方法(因为这是一个 Web 表单项目)。它很长,我知道很丑,但它的重点是捕捉用户输入的内容并建立我们的弹性搜索结果。尽管我很讨厌它,但它目前是我们所拥有的:
public List<ElasticSearchResult> DoSearch(string searchQuery, int module, int resultsToReturn = 0)
{
//get tag
//user can add a tag to the search, "SearchQuery [C++]" looking for anything with SearchQuery that is tagged C++
string tag = string.Empty;
int pos = searchQuery.IndexOf('[', 0);
if (pos > -1)
{
int pos2 = searchQuery.IndexOf(']', pos);
if (pos2 > -1)
{
//we've got [ ]
tag = searchQuery.Substring(pos + 1, pos2 - pos - 1).ToLower();
}
if (pos > 0)
searchQuery = searchQuery.Substring(0, pos - 1);
}
//The search engine only sees /t, not a tab, so replace it with a space
searchQuery = searchQuery.Replace("\t", " ");
//Reserved characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
//Remove reserved characters the user won't use
var reserverCharacters = new List<string>
{
"+",
"&&",
"||",
"(",
")",
"{",
"}",
"[",
"]",
"^",
"~",
":",
"\\",
"/"
};
searchQuery = reserverCharacters.Aggregate(searchQuery,
(current, character) => current.Replace(character, ""));
//To prevent search errors, trim * and whitespace
searchQuery = searchQuery.Trim(' ', '*');
//Define the descriptors so they can be reused throughout this function
var companySearchDescriptor = new SearchDescriptor<ElasticSearchCompany>();
companySearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 1.6)
.Add(f => f.Summary.Suffix("ngram"), 1.5)
.Add(f => f.Description, 1.0)
.Add(f => f.City.Suffix("ngram"), 1.1)
.Add(f => f.City, 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var contactSearchDescriptor = new SearchDescriptor<ElasticSearchContact>();
contactSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 1.6)
.Add(f => f.Summary.Suffix("ngram"), 1.5)
.Add(f => f.Description, 1.0)
.Add(f => f.Email, 1.1)
.Add(f => f.Email.Suffix("ngram"), 1.0)
.Add(f => f.Title, 1.1)
.Add(f => f.Title.Suffix("ngram"), 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var userSearchDescriptor = new SearchDescriptor<ElasticSearchUser>();
userSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 1.6)
.Add(f => f.Summary.Suffix("ngram"), 1.5)
.Add(f => f.Description, 1.0)
.Add(f => f.Email, 1.1)
.Add(f => f.Email.Suffix("ngram"), 1.0)
.Add(f => f.Title, 1.1)
.Add(f => f.Title.Suffix("ngram"), 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var corporationSearchDescriptor = new SearchDescriptor<ElasticSearchCorporation>();
corporationSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 1.6)
.Add(f => f.Summary.Suffix("ngram"), 1.5)
.Add(f => f.Description, 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var documentSearchDescriptor = new SearchDescriptor<ElasticSearchDocument>();
documentSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 2.0)
.Add(f => f.Summary.Suffix("ngram"), 1.5)
.Add(f => f.Title, 1.4)
.Add(f => f.Title.Suffix("ngram"), 1.0)
.Add(f => f.Description, 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var checklistSearchDescriptor = new SearchDescriptor<ElasticSearchChecklist>();
checklistSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 2.0)
.Add(f => f.Summary.Suffix("ngram"), 1.9)
.Add(f => f.Summary.Suffix("ngram_no_punct"), 1.9)
.Add(f => f.Id, 2.0)
.Add(f => f.Milestone, 1.6)
.Add(f => f.RecordName, 1.5)
.Add(f => f.RecordName.Suffix("ngram"), 1.4)
.Add(f => f.RecordName.Suffix("ngram_no_punct"), 1.4)
.Add(f => f.ParentName, 1.3)
.Add(f => f.ParentName.Suffix("ngram"), 1.2)
.Add(f => f.ParentName.Suffix("ngram_no_punct"), 1.2)
.Add(f => f.Description, 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
var issueSearchDescriptor = new SearchDescriptor<ElasticSearchIssue>();
issueSearchDescriptor
.Size(35)
.Query(q => q
.QueryString(query => query
.AnalyzeWildcard()
.OnFieldsWithBoost(b => b
.Add(f => f.Summary, 1.3)
.Add(f => f.Summary.Suffix("ngram"), 1.2)
.Add(f => f.Description, 1.0)
.Add(f => f.IssueNumber, 2.0)
.Add(f => f.LessonLearned, 1.0)
.Add(f => f.Resolution, 1.0)
.Add(f => f.Type, 1.0)
.Add(f => f.Tags, 1.0))
.Query(searchQuery)));
//SIMILIAR CODE FOR ALL OTHER MODULES BASED ON ISSUE ABOVE
//TOO MUCH CODE TO POST
//Need the client before you can make a call to the search servers
var esClient = Code.Utilities.GetElasticClient();
if (esClient == null) return null;
IMultiSearchResponse result;
//Each case(other than default) handles if the user chose to only search for a single type of module
switch (module)
{
case (int) Module.Company:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchCompany>(a => companySearchDescriptor));
break;
}
case (int) Module.Contact:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchContact>(a => contactSearchDescriptor));
break;
}
case (int) Module.Corporate:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchCorporation>(a => corporationSearchDescriptor));
break;
}
case (int) Module.Checklist:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchChecklist>(a => checklistSearchDescriptor));
break;
}
case (int) Module.Document:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchDocument>(a => documentSearchDescriptor));
break;
}
case (int) Module.Issue:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchIssue>(a => issueSearchDescriptor));
break;
}
case (int) Module.TaskReminder:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchTaskReminder>(a => taskSearchDescriptor));
break;
}
case (int) Module.LessonLearned:
{
result =
esClient.MultiSearch(search => search
.Search<ElasticSearchLessonLearned>(a => lessonLearnedSearchDescriptor));
break;
}
case (int) Module.Program:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchProgram>(a => programSearchDescriptor));
break;
}
case (int) Module.Project:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchProject>(a => projectSearchDescriptor));
break;
}
case (int) Module.SubProject:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchSubProject>(a => subprojectSearchDescriptor));
break;
}
case (int) Module.ServiceOrder:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchServiceOrder>(a => serviceOrderDescriptor));
break;
}
case (int) Module.Meeting:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchMeeting>(a => meetingSearchDescriptor));
break;
}
case (int) Module.Tip:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchTip>(a => tipSearchDescriptor));
break;
}
case (int) Module.Incident:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchIncident>(a => incidentSearchDescriptor));
break;
}
case (int) Module.Material:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchMaterial>(a => materialSearchDescriptor));
break;
}
case (int) Module.Training:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchTraining>(a => trainingSearchDescriptor));
break;
}
case (int) Module.User:
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchUser>(a => userSearchDescriptor));
break;
}
default: //Handles the "All" type search
{
//LessonLearned results are Issues, don't want to search them twice in "All" type search
if (MySession.Current.User.IsAdmin)
{
//allow user search
result = esClient.MultiSearch(search => search
.Search<ElasticSearchCompany>(a => companySearchDescriptor)
.Search<ElasticSearchContact>(a => contactSearchDescriptor)
.Search<ElasticSearchUser>(a => userSearchDescriptor)
.Search<ElasticSearchCorporation>(a => corporationSearchDescriptor)
.Search<ElasticSearchChecklist>(a => checklistSearchDescriptor)
.Search<ElasticSearchDocument>(a => documentSearchDescriptor)
.Search<ElasticSearchIssue>(a => issueSearchDescriptor)
.Search<ElasticSearchTaskReminder>(a => taskSearchDescriptor)
.Search<ElasticSearchProgram>(a => programSearchDescriptor)
.Search<ElasticSearchProject>(a => projectSearchDescriptor)
.Search<ElasticSearchSubProject>(a => subprojectSearchDescriptor)
.Search<ElasticSearchServiceOrder>(a => serviceOrderDescriptor)
.Search<ElasticSearchMeeting>(a => meetingSearchDescriptor)
.Search<ElasticSearchTraining>(a => trainingSearchDescriptor)
.Search<ElasticSearchIncident>(a => incidentSearchDescriptor)
.Search<ElasticSearchMaterial>(a => materialSearchDescriptor)
.Search<ElasticSearchTip>(a => tipSearchDescriptor));
}
else
{
result = esClient.MultiSearch(search => search
.Search<ElasticSearchCompany>(a => companySearchDescriptor)
.Search<ElasticSearchContact>(a => contactSearchDescriptor)
.Search<ElasticSearchCorporation>(a => corporationSearchDescriptor)
.Search<ElasticSearchChecklist>(a => checklistSearchDescriptor)
.Search<ElasticSearchDocument>(a => documentSearchDescriptor)
.Search<ElasticSearchIssue>(a => issueSearchDescriptor)
.Search<ElasticSearchTaskReminder>(a => taskSearchDescriptor)
.Search<ElasticSearchProgram>(a => programSearchDescriptor)
.Search<ElasticSearchProject>(a => projectSearchDescriptor)
.Search<ElasticSearchSubProject>(a => subprojectSearchDescriptor)
.Search<ElasticSearchServiceOrder>(a => serviceOrderDescriptor)
.Search<ElasticSearchMeeting>(a => meetingSearchDescriptor)
.Search<ElasticSearchTraining>(a => trainingSearchDescriptor)
.Search<ElasticSearchIncident>(a => incidentSearchDescriptor)
.Search<ElasticSearchMaterial>(a => materialSearchDescriptor)
.Search<ElasticSearchTip>(a => tipSearchDescriptor)
);
}
break;
}
}
List<ElasticSearchResult> esSearchResultList =
ParseSearchResults(result, MySession.Current.User.IsAdmin).ToList();
if (!string.IsNullOrEmpty(tag) && tag.Length > 0)
esSearchResultList = esSearchResultList.Where(x => x.Tags.Any(a => a == tag)).ToList();
//we need to reserve the order of the result
//so we added a new property called sort order which we will update below
int counter = 0;
foreach (var esr in esSearchResultList)
{
esr.SortOrder = counter;
counter++;
}
//Remove any results the user doesn't have permission to see
var entityResultList = new List<ElasticSearchResult>();
//this is a inteermediate place holder for records
var filteredResultList = new List<ElasticSearchResult>();
//this will have the final result after filtering
//get the search entity list
var entityList = CachedData.EntitySearchList;
using (var db = DataCenterAccess.NewConnection())
{
//loop thru the entity search to filter what the current user can /cannot see
foreach (var entity in entityList)
{
//get user permission for current entity
Answer permission =
(Answer) MySession.Current.Permission.GetPermission(entity.EntityID, (int) ActionEnum.View);
if (permission == Answer.Yes)
{
//if yes get all records for that entity type
entityResultList = esSearchResultList.Where(x => x.EntityId == entity.EntityID).ToList();
}
else if (permission == Answer.Originator)
{
//if originator ; get only record the user can see for that entity type
entityResultList =
esSearchResultList.Where(
x =>
x.EntityId == entity.EntityID &&
x.AddedByUserId == MySession.Current.User.UserId).ToList();
}
//add the result to the final result list
filteredResultList.AddRange(entityResultList);
}
//now we need to isolate the records that are private based on the team record security
var privateRecords = filteredResultList.Where(x => x.Private).ToList();
//also make sure the filtered list does not have any of the private records
filteredResultList = filteredResultList.Where(x => !x.Private).ToList();
if (privateRecords.Any())
{
//if we have any private records
//we need to loop thru and see if the user is on a team that has access to the record
foreach (var esr in privateRecords)
{
if (RIMSBL.Code.DAL.UserHasTeamPermission(db, esr.EntityId, esr.RecordId,
MySession.Current.User.UserId))
filteredResultList.Add(esr);
}
}
}
//0 means return all. If not 0, then limit to that number of search results
if (resultsToReturn != 0)
{
filteredResultList = filteredResultList.OrderBy(x => x.SortOrder).Take(resultsToReturn).ToList();
}
return filteredResultList;
}
我不确定问题是否与数据有关,因为我们只处理数十万条记录中的某些内容(跨所有模块)。我们要做的一件事是,当您在搜索文本框中输入字母时,我们会不断调用 DoSearch,完成后它会返回一个状态码。因此,如果您输入 7 个字符,则会在第 3 个字符上调用搜索,直到第 7 个字符(总共 5 次),如 chrome 控制台所示:
我唯一能想到的另一件事是也许我们只需要更好的硬件。该服务器统计:
我知道搜索服务器内存不足,但在我请求其他硬件之前,我想看看我是否缺少任何东西。
【问题讨论】:
-
优化的第一条规则是“衡量它”。您是否尝试过测量究竟需要多少时间?后续请求是否实际上会减慢第一个请求(即仅测量 3 个字母的响应时间与 3 个字母的响应时间,然后您继续输入)?需要测量的明显的事情是:
switch (module)下的 MultiSearch 的时间,ParseSearchResults的跟踪标记过滤的时间(顺便说一句,你不能把它移到 ElasticSearch 吗?),以及最终块的时间可能访问D B。然后您(和我们)可以看到需要进一步关注的内容。 -
我要做的第一件事是使用 Chrome DevTools 来确定您的瓶颈是在后端还是前端。 (序列化结果或渲染它们)。我们可以查看其中一个
DoSearch电话的详细信息吗?
标签: jquery search autocomplete