/// <summary> /// 全文検索の実行 /// </summary> /// <param name="query"></param> /// <param name="offset"></param> /// <returns></returns> public Result Search( string queryKeyword , string queryFileName = null , string queryBody = null , string queryUpdated = null , int offset = 0 , string selectedFormat = null , string selectedFolderLabel = null , string selectedOrderType = null , string selectedView = null ) { var groongaQueries = new List <string>(); var groongaFilters = new List <string>(); var querySubMessages = new List <string>(); // キーワード、ファイル名、本文での絞り込みをエスケープして追加 if (!string.IsNullOrWhiteSpace(queryKeyword)) { // Groongaのクエリ構文は一部のみ有効 // バックスラッシュ、シングルクォート、コロンなどの特殊記号はエスケープする // 有効なのは " - ( ) のみ groongaQueries.Add(queryKeyword .Replace(@"\", @"\\") .Replace("'", @"\'") .Replace(":", @"\:") .Replace("+", @"\+")); } if (!string.IsNullOrWhiteSpace(queryFileName)) { groongaQueries.Add(string.Format("{0}:@{1}" , Column.Documents.FILE_NAME , Groonga.Util.EscapeForQuery(queryFileName))); querySubMessages.Add(string.Format("ファイル名: 「{0}」", queryFileName)); } if (!string.IsNullOrWhiteSpace(queryBody)) { groongaQueries.Add(string.Format("{0}:@{1}" , Column.Documents.BODY , Groonga.Util.EscapeForQuery(queryBody))); querySubMessages.Add(string.Format("本文: 「{0}」", queryBody)); } // 拡張子での絞り込みを追加 (フォーマットから生成する) if (selectedFormat != null) { if (selectedFormat == "その他") { // 「その他」が指定された場合は、登録されている拡張子をすべて除外 var excludeExtNames = new List <string>(); foreach (var format in App.Formats) { excludeExtNames.AddRange(format.Extensions); } var strings = excludeExtNames.Select(e => string.Format("\"{0}\"", e)); if (!strings.Any()) { strings = new[] { "\"\"" } } ; groongaFilters.Add(string.Format("!in_values({0},{1})" , Column.Documents.EXT , string.Join(", ", strings)) ); querySubMessages.Add("ファイル形式: その他"); } else { var targetExtNames = new List <string>(); foreach (var format in App.Formats.Where(f => f.Label == selectedFormat)) { targetExtNames.AddRange(format.Extensions); } ; var strings = targetExtNames.Select(e => string.Format("\"{0}\"", e)); if (!strings.Any()) { strings = new[] { "\"\"" } } ; groongaFilters.Add(string.Format("in_values({0},{1})" , Column.Documents.EXT , string.Join(", ", strings)) ); querySubMessages.Add(string.Format("ファイル形式: {0}", selectedFormat)); } } // フォルダラベルでの絞り込みを追加 if (selectedFolderLabel != null) { groongaQueries.Add(string.Format("{0}:{1}" , Column.Documents.FOLDER_LABELS , Groonga.Util.EscapeForQuery(selectedFolderLabel))); querySubMessages.Add(string.Format("フォルダラベル: {0}", selectedFolderLabel)); } // 日付範囲の指定があれば、その範囲を追加する DateTime?updatedLeft = null; string updatedRangeCaption = null; switch (queryUpdated) { case "day": updatedLeft = DateTime.Now.AddDays(-1); updatedRangeCaption = "1日以内"; break; case "week": updatedLeft = DateTime.Now.AddDays(-7); updatedRangeCaption = "1週間以内"; break; case "month": updatedLeft = DateTime.Now.AddMonths(-1); updatedRangeCaption = "1ヶ月以内"; break; case "half_year": updatedLeft = DateTime.Now.AddMonths(-6); updatedRangeCaption = "半年以内"; break; case "year": updatedLeft = DateTime.Now.AddYears(-1); updatedRangeCaption = "1年以内"; break; case "3years": updatedLeft = DateTime.Now.AddYears(-3); updatedRangeCaption = "3年以内"; break; } if (updatedLeft != null) { groongaFilters.Add(string.Format("file_updated_at > \"{0}\"", Groonga.Util.ToExprTimeFormat(updatedLeft.Value))); querySubMessages.Add(string.Format("更新日付: {0}", updatedRangeCaption)); } var matchColumns = new[] { Column.Documents.FILE_NAME + " * 1000" , string.Format("scorer_tf_idf({0})", Column.Documents.BODY) }; var columns = new List <Groonga.DynamicColumn>(); // 最終更新からの経過時間 var elapsedExpr = string.Format("((now() - {0}) / (60 * 60))", Column.Documents.FILE_UPDATED_AT); columns.Add(new Groonga.DynamicColumn( "elapsed_hour_from_file_updated" , Groonga.Stage.FILTERED , Groonga.DataType.Int64 , elapsedExpr )); // 鮮度補正 (最終更新からの経過時間によるスコア補正倍率) var rateData = new[] { Tuple.Create(12, 30.0) // 半日以内なら×30 , Tuple.Create(24, 20.0) // 1日以内なら×20 , Tuple.Create(24 * 3, 10.0) // 3日以内なら×10 , Tuple.Create(24 * 7, 7.0) // 1週間以内なら×7 , Tuple.Create(24 * 15, 5.0) // 15日以内なら×5 , Tuple.Create(24 * 30, 3.0) // 30日(約1ヶ月)以内なら×3 , Tuple.Create(24 * 60, 2.0) // 60日(約2ヶ月)以内なら×2 , Tuple.Create(24 * 90, 1.5) // 90日(約3ヶ月)以内なら×1.5 }; var rateExpr = "1.0"; foreach (var t in rateData.Reverse()) { var border = t.Item1; var rate = t.Item2; rateExpr = string.Format("({0} <= {1} ? {2} : {3})", elapsedExpr, border, rate, rateExpr); } columns.Add(new Groonga.DynamicColumn( "freshness_score_rate" , Groonga.Stage.FILTERED , Groonga.DataType.Float , rateExpr )); // 最終スコア columns.Add(new Groonga.DynamicColumn( "final_score" , Groonga.Stage.FILTERED , Groonga.DataType.Int64 , string.Format("{0} * {1}", Groonga.VColumn.SCORE, rateExpr) )); // 並び順の設定。並び順が指定されていれば、その並び順を優先 var sortKeys = new List <string>(); switch (selectedOrderType) { case OrderType.FILE_PATH: sortKeys.Add(Column.Documents.FILE_PATH); break; case OrderType.FILE_UPDATED_DESC: sortKeys.Add("-" + Column.Documents.FILE_UPDATED_AT); break; } // 標準では最終スコア(関連度+鮮度)が高い順に並べる sortKeys.Add("-final_score"); // SELECT実行 var joinedQuery = string.Join(" ", groongaQueries.Select(q => "(" + q + ")")); var joinedFilter = string.Join(" && ", groongaFilters.Select(q => "(" + q + ")")); var pageSize = (selectedView == ViewType.LIST ? App.UserSettings.DisplayPageSizeForListView : App.UserSettings.DisplayPageSizeForNormalView); Groonga.SelectResult selectRes = null; try { selectRes = App.GM.Select( Table.Documents , query: joinedQuery , filter: joinedFilter , offset: offset , limit: pageSize //, drilldown: new[] { Column.Documents.EXT, Column.Documents.FILE_UPDATED_YEAR } , drilldown: new[] { Column.Documents.EXT, Column.Documents.FOLDER_LABELS } , drilldownSortKeys: new[] { Column.Documents.KEY } , sortKeys: sortKeys.ToArray() , matchColumns: matchColumns , outputColumns: new[] { Column.Documents.KEY , Column.Documents.FILE_PATH , Groonga.VColumn.SCORE // 鮮度補正を加味していない生のスコア , Column.Documents.TITLE , Column.Documents.EXT , Column.Documents.FILE_NAME , Column.Documents.FILE_UPDATED_AT , Column.Documents.SIZE , Groonga.Function.SnippetHtml(Column.Documents.FILE_NAME) , Groonga.Function.SnippetHtml(Column.Documents.TITLE) , Groonga.Function.SnippetHtml(Column.Documents.BODY) , "elapsed_hour_from_file_updated" , "freshness_score_rate" , "final_score" } , columns: columns ); } catch (GroongaCommandError ex) { if (ex.ReturnCode == Groonga.CommandReturnCode.GRN_SYNTAX_ERROR || ex.ReturnCode == Groonga.CommandReturnCode.GRN_INVALID_ARGUMENT) { // シンタックスエラーの場合、クエリ構文にエラーがあるかどうかをチェック // 引数を単純化した上でもう一度Selectを実行し、同じエラーが出るならクエリ構文が不正とみなす Logger.Debug(ex.ToString()); try { App.GM.Select(Table.Documents, limit: 0, outputColumns: new[] { Groonga.VColumn.ID }, query: joinedQuery); } catch (GroongaCommandError) { if (ex.ReturnCode == Groonga.CommandReturnCode.GRN_SYNTAX_ERROR || ex.ReturnCode == Groonga.CommandReturnCode.GRN_INVALID_ARGUMENT) { var errorRet = new Result { success = false, errorMessage = "検索語の解析時にエラーが発生しました。\n単語をダブルクォート (\") で囲んで試してみてください。" }; return(errorRet); } throw; } } throw; } var searchResult = selectRes.SearchResult; var records = new List <Record>(); //var imgConv = new ImageConverter(); var cryptProvider = new SHA1CryptoServiceProvider(); var thumbnailDirPath = App.ThumbnailDirPath; foreach (var selectRec in searchResult.Records) { var rec = new Record { key = (string)selectRec.Key, file_name = (string)selectRec[Column.Documents.FILE_NAME], file_path = (string)selectRec[Column.Documents.FILE_PATH], title = (string)selectRec[Column.Documents.TITLE] }; if (!string.IsNullOrEmpty(rec.file_path)) { rec.folder_path = Path.GetDirectoryName(rec.file_path); } rec.base_score = selectRec.GetIntValue(Groonga.VColumn.SCORE).Value; rec.final_score = selectRec.GetIntValue("final_score").Value; var titleSnippets = selectRec[Groonga.Function.SnippetHtml(Column.Documents.TITLE)] as object[]; if (titleSnippets != null) { rec.title_snippets = titleSnippets.Cast <string>().ToArray(); } var fileNameSnippets = selectRec[Groonga.Function.SnippetHtml(Column.Documents.FILE_NAME)] as object[]; if (fileNameSnippets != null) { rec.file_name_snippets = fileNameSnippets.Cast <string>().ToArray(); } var bodySnippets = selectRec[Groonga.Function.SnippetHtml(Column.Documents.BODY)] as object[]; if (bodySnippets != null) { rec.body_snippets = bodySnippets.Cast <string>().ToArray(); } rec.ext = (string)selectRec[Column.Documents.EXT]; var updated = Groonga.Util.FromUnixTime((double)selectRec[Column.Documents.FILE_UPDATED_AT]); rec.timestamp_updated_caption = updated.ToString("yyyy/MM/dd(ddd) HH:mm"); rec.timestamp_updated_caption_for_list_view = updated.ToString("yyyy/MM/dd HH:mm"); rec.size_caption = Util.FormatFileSizeByKB((long)selectRec[Column.Documents.SIZE]); // サムネイル画像があれば、そのパスも設定 var thumbnailPath = Path.Combine(thumbnailDirPath, Util.HexDigest(cryptProvider, rec.key) + ".png"); if (File.Exists(thumbnailPath)) { rec.thumbnail_path = thumbnailPath; } else { rec.thumbnail_path = null; } records.Add(rec); } // ドリルダウン結果(拡張子ごとの件数)を元に、フォーマット絞り込み用のデータを作成 var formatDrilldownLinks = new List <FormatDrilldownLink>(); var formatCounts = new Dictionary <string, long>(); var formatToLabelMap = new Dictionary <string, string>(); foreach (var format in App.Formats) { foreach (var ext in format.Extensions) { formatToLabelMap[ext] = format.Label; } } foreach (var extRec in selectRes.DrilldownResults[0].Records) { var ext = (string)extRec.Key; var label = (formatToLabelMap.ContainsKey(ext) ? formatToLabelMap[ext] : "その他"); if (!formatCounts.ContainsKey(label)) { formatCounts[label] = 0; } formatCounts[label] += extRec.GetIntValue(Groonga.VColumn.NSUBRECS).Value; } foreach (var formatLabel in formatCounts.Keys) { var link = new FormatDrilldownLink() { name = formatLabel , caption = formatLabel , nSubRecs = formatCounts[formatLabel] }; formatDrilldownLinks.Add(link); } // ドリルダウン結果(フォーマットごとの件数)を元に、フォーマット絞り込み用のデータを作成 var folderLabelDrilldownLinks = new List <FolderLabelDrilldownLink>(); foreach (var rec in selectRes.DrilldownResults[1].Records) { var link = new FolderLabelDrilldownLink() { folderLabel = (string)rec.Key , nSubRecs = rec.NSubRecs }; folderLabelDrilldownLinks.Add(link); } string searchResultMessage; var searchResultSubMessage = ""; if (searchResult.NHits == 0) { searchResultMessage = "文書が見つかりませんでした。"; } else { if (string.IsNullOrWhiteSpace(queryKeyword)) { searchResultMessage = string.Format("計{0}件見つかりました。", searchResult.NHits); } else { searchResultMessage = string.Format("「{0}」で検索して、計{1}件見つかりました。", queryKeyword, searchResult.NHits); } } if (querySubMessages.Count >= 1) { searchResultSubMessage = string.Format("({0})", string.Join("、", querySubMessages)); } var ret = new Result() { success = true , nHits = searchResult.NHits , records = records.ToArray() , searchResultMessage = searchResultMessage , searchResultSubMessage = searchResultSubMessage , formatDrilldownLinks = formatDrilldownLinks.OrderByDescending(l => l.nSubRecs).ToList() // 件数の多い順で並べる , folderLabelDrilldownLinks = folderLabelDrilldownLinks.OrderByDescending(l => l.nSubRecs).ToList() // 件数の多い順で並べる , orderList = OrderList , pageSize = pageSize }; return(ret); }
public string GetHighlightedBody(string key, string queryKeyword, string queryBody) { return(App.ExecuteInExceptionCatcher <string>(() => { var groongaQueries = new List <string>(); var groongaFilters = new List <string>(); if (!string.IsNullOrWhiteSpace(queryKeyword)) { // Groongaのクエリ構文有効。ただしバックスラッシュとシングルクォートはエスケープする groongaQueries.Add(queryKeyword.Replace(@"\", @"\\").Replace("'", @"\'")); } if (!string.IsNullOrWhiteSpace(queryBody)) { groongaQueries.Add(string.Format("{0}:@{1}" , Column.Documents.BODY , Groonga.Util.EscapeForQuery(queryBody))); } groongaFilters.Add(string.Format("{0} == {1}", Column.Documents.KEY, Groonga.Util.EscapeForScript(key))); // SELECT実行 var joinedQuery = string.Join(" ", groongaQueries.Select(q => "(" + q + ")")); var joinedFilter = string.Join(" && ", groongaFilters.Select(q => "(" + q + ")")); Groonga.SelectResult selectRes = null; var expr = "highlight_html(body)"; selectRes = App.GM.Select( Table.Documents , query: joinedQuery , filter: joinedFilter , matchColumns: new[] { "body" } , outputColumns: new[] { expr, Groonga.VColumn.SCORE } , limit: 1 ); var searchResult = selectRes.SearchResult; if (searchResult.NHits >= 1) { // ハイライトHTMLを取得 var html = searchResult.Records[0].GetTextValue(expr); // 行単位で分割し、「ハイライトを含む行数」の前後N行を取得 var lines = html.Split(new[] { "\r\n" }, StringSplitOptions.None).ToList(); var segments = new List <Tuple <int, int> >(); int?firstMatchInSegment = null; int?lastMatchInSegment = null; var segmentRange = 3; for (var i = 0; i < lines.Count; i++) { var line = lines[i]; if (line.Contains("<span class=\"keyword\">")) { firstMatchInSegment = firstMatchInSegment ?? i; // セグメントがまだ開始していなければ開始 lastMatchInSegment = i; } // 最後にハイライトが含まれていた行から、規定行数以上離れていれば、セグメント確定 if (lastMatchInSegment != null && i > lastMatchInSegment + segmentRange) { var start = firstMatchInSegment.Value - segmentRange; if (start < 0) { start = 0; } var end = lastMatchInSegment.Value + segmentRange; segments.Add(Tuple.Create(start, end)); // マッチ情報初期化 lastMatchInSegment = null; firstMatchInSegment = null; } } // ループ終了時に終了していない情報があれば、同様にセグメント確定 if (lastMatchInSegment != null) { var start = firstMatchInSegment.Value - segmentRange; if (start < 0) { start = 0; } var end = lines.Count - 1; segments.Add(Tuple.Create(start, end)); } // ハイライトHTMLの抜粋を生成 var outLines = new List <string>(); foreach (var segment in segments) { outLines.Add("<div style=\"border: 1px solid silver; margin: 0; padding: 1em; font-size: small; overflow-y: auto;\">"); outLines.AddRange(lines.GetRange(segment.Item1, segment.Item2 - segment.Item1 + 1)); outLines.Add("</div>"); } // ハイライトされたHTML var res = new { body = string.Join("\r\n", outLines), hitCount = searchResult.Records[0].GetIntValue(Groonga.VColumn.SCORE) }; return JsonConvert.SerializeObject(res); } else { return JsonConvert.SerializeObject(null); } })); }