/// <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);
        }
Example #2
0
            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);
                    }
                }));
            }