jTemplatesを介したSphinx for ASP.NET



趣味があります-飲み物や食べ物を大量に販売するオンラインストアを開発することです。

当社の製品は、サプライヤを引き付け、店舗に製品を配置することで表示されます。

クライアントは、翌日配達で大量の商品を注文するレストランやカフェの所有者です。

商品のポジション数が2万を超えると、特にサプライヤが商品にエラーをロードした場合、または商品の名前がラテン語/キリル文字であった場合、MS SQLでのlikeによる検索が間違っすぎました。 ラテン語-シリリック語-ラテン語の変換、文法エラーの修正を伴う検索手順での1か月のさまざまなトリックの後、私たちはこれが検索が発展する行き止まりの方法であることに最終的に気付きました。



ソリューションを検索する



顔をしかめ、私たちは同じウィキマートで他のプロジェクトで同様の問題がどのように解決されるかを探ることにしました。 enましいことに、それらの検索はうまく機能し、商品の言葉の間違いを修正しました。 たとえば、コカコーラのリクエストで、コカコーラとコカインの両方を見つけることができました。 どんな種類のDBMSが彼らにとってとても魔法なのか、私たちは叫んだ。

インターネットで技術的な解決策を短時間検索した後、フルテキスト検索エンジンが必要であることがわかりました。 雲の上を飛ぶと、すぐに「人の検索」を実現できるでしょう。そして、 ファセットフィルターが無料のパイとしてどのように表示されるかさえ、私たちはそれを実装するものを探し始めました。

そして、判明したように、フルテキスト検索は既にDBMSに組み込まれているMS SQL 2008 Advanced Servicesにあります! 1週間MS SQLを掘り下げ、ファセットの形で無料のパイを見つけられなかった後、魔法のLucene.NETSolrSphinx に関する記事に出会いまし



エンジンの選択



上記のエンジンの小規模なテストの後、次の基準に従ってSphinxを選択しました。

  1. Microsoft Windowsで動作します
  2. 開発者サポートと大規模なFAQがあります
  3. MS SQLで動作します
  4. .NETがエンジンと通信するための既製のアダプターがあります
  5. ファセットがあります


ビジネスへ



スフィンクスの構成


configは 、実際には特別なものではありません。

形態を使用して、stem_enru、soundex、metaphone(これについてはPumaに感謝します)

MS SQLデータベースに接続し、Sphinxが定期的にプルする製品でViewを使用します。 しかし、私たちはもう少し進んで、製品だけでなくブランド、サプライヤー、カテゴリーも検索するためにスフィンクスの領域を拡大しました。



ASP.NETでSphinxを操作する


Sphinxを使用するには、オープンソースのSphinx.Clientを使用します。

Sphinx.Clientを介してSphinxを操作するためのSphinxHelperヘルパークラス。

スフィンクスヘルパー
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sphinx.Client.Connections; using Sphinx.Client.Commands.Search; using System.Collections; using Sphinx.Client.Commands.Collections; using Sphinx.Client.Commands.Attributes.Filters; namespace Project.Helpers { public class SphinxHelper { private static ConnectionBase CreateConnection() { PersistentTcpConnection p_connection = new PersistentTcpConnection("127.0.0.1", 9312); p_connection.ConnectionTimeout = 10000; return p_connection; } public static IList<SearchQueryResult> Query(string queryText, string indexes, int limitPerIndex) { return Query("", queryText, indexes, null, limitPerIndex, 0, MatchMode.Extended2, MatchRankMode.WordCount, ResultsSortMode.Extended, "@weight DESC", ""); } public static IList<SearchQueryResult> Query(string select, string match, string indexes, AttributeFilterList filters, int pageSize, int offset, MatchMode matchMode, MatchRankMode rankingMode, ResultsSortMode sortMode, string sortBy, string groupBy) { IEnumerable<string> p_idxArray = indexes.Split(','); SearchQuery p_query = null; pageSize = pageSize <= 0 ? 99999999 : pageSize; IList<SearchQueryResult> p_ret = new System.Collections.Generic.List<SearchQueryResult>(); using (ConnectionBase connection = CreateConnection()) { SearchCommand p_search = new SearchCommand(connection); foreach (string p_idx in p_idxArray) { p_query = new SearchQuery(match, p_idx); p_query.Select = select; p_query.MatchMode = matchMode; p_query.RankingMode = rankingMode; p_query.SortMode = sortMode; p_query.SortBy = sortBy; if (!String.IsNullOrEmpty(groupBy)) { p_query.GroupBy = groupBy; p_query.GroupSort = sortBy; p_query.GroupFunc = ResultsGroupFunction.Attribute; if (!String.IsNullOrEmpty(sortBy)) { p_query.SortBy = string.Empty; } } p_query.Limit = pageSize; p_query.Offset = offset; //   ,   if (filters != null && filters.Count > 0) foreach (AttributeFilterBase p_filter in filters) p_query.AttributeFilters.Add(p_filter); p_query.Select = select; //  ,     //search.QueryList.Add(p_query); p_search.QueryList.Clear(); p_search.QueryList.Add(p_query); p_search.Execute(); foreach (SearchQueryResult p_result in p_search.Result.QueryResults) p_ret.Add(p_result); } return p_ret; } } } }
      
      







SearchQueryResultからユーザーのブラウザーへ


ブランドと製品を含む検索ページを生成するとき、 jTemplatesを使用します 。このテンプレートは、Webサービスからデータを受け取り、SphinxHelperをプルします。

検索ページの生成
  //   //brandId -    //sellerId -    //categoryId -    //specIds -  facets //searchText -    this.GetGroups = function (brandId, sellerId, categoryId, specIds, searchText) { var waiter = $('#waiter_' + brandId); waiter.css({ visibility: 'visible' }); var brandGroups = $('#brandGroups_' + brandId); brandGroups.attr('loaded', true); $.ajax({ type: "POST", context: { brandId: brandId, sellerId: sellerId, categoryId: categoryId, specIds: specIds, searchText: searchText }, url: currentHost() + "WebServices/Products.asmx/GetBrandGroups", data: "{brandID:'" + brandId + "',sellerID:'" + sellerId + "', categoryID:'" + categoryId + "', specIds:'" + specIds + "',searchText:'" + searchText + "'}", contentType: "application/json; charset=utf-8", dataType: "json", success: brandsInRow2.GroupCallSuccess, error: brandsInRow2.GroupCallError }); } this.GroupCallError = function (request, status, error) { alert(request.responseText); } this.GroupCallSuccess = function (data, status) { var data_decoded = $.parseJSON(data.d); var brandGroups = $('#brandGroups_' + this.brandId); brandGroups.setTemplate($("#templateProducts").html()); brandGroups.setParam('GetProductPriceActuality', brandsInRow2.GetProductPriceActuality); brandGroups.setParam('GetProductPriceActuality1', brandsInRow2.GetProductPriceActuality1); brandGroups.setParam('GetSpecDescription', brandsInRow2.GetSpecDescription); brandGroups.setParam('GetBrandPriceName', brandsInRow2.GetBrandPriceName); brandGroups.setParam('GetOrderProductFrameLink', brandsInRow2.GetOrderProductFrameLink); brandGroups.setParam('GetSellerInfoFrameLink', brandsInRow2.GetSellerInfoFrameLink); brandGroups.setParam('GetMessageSendFrameLink', brandsInRow2.GetMessageSendFrameLink); brandGroups.processTemplate(data_decoded); brandGroups.css({ display: 'block' }); //   brandsInRow2.InitTips(); var waiter = $('#waiter_' + this.brandId); waiter.css({ visibility: 'hidden' }); } this.GetLinkOfferName = function (prodCnt, sellersCnt) { var p_offers = prodCnt + ' ' + formatToRussian1(prodCnt, ""); if (sellersCnt > 1) p_offers = p_offers + ' ' + formatToRussian(sellersCnt, ""); return p_offers; } this.GetBrandCountName = function (brandsCnt) { return brandsCnt.toString() + ' ' + formatToRussian(brandsCnt, ""); } this.GetBrandPriceName = function (minPrice, maxPrice) { if (minPrice == maxPrice) return formatPrice(minPrice); return ' ' + formatPrice(minPrice) + '  ' + formatPrice(maxPrice) } this.GetProductPriceActuality = function (product) { return product.DaysUpdated > 30 ? "      ,       " + product.CompanyName + "." : ""; } this.GetProductPriceActuality1 = function (product) { return product.DaysUpdated > 30 ? "" : "hidden"; } this.GetSpecDescription = function (specs) { var escaped = specs; var findReplace = [[/&/g, "&"], [/</g, "<"], [/>/g, ">"], [/"/g, '"'], [/'/g, "'"]] for (var item in findReplace) escaped = escaped.replace(findReplace[item][0], findReplace[item][1]); return escaped; } this.GetOrderProductFrameLink = function (productId) { return currentHost() + "OrderProductFrame.aspx?ProductID=" + productId; } this.GetSellerInfoFrameLink = function (sellerId) { return currentHost() + "SellerShortInfoFrame.aspx?SellerID=" + sellerId; } this.GetMessageSendFrameLink = function (productId) { return currentHost() + "MessageSendFrame.aspx?ProductID=" + productId; }
      
      







また、Webサービスからデータを受信する検索バーにオートコンプリートを実装することも非常に簡単でした。

オートコンプリート
 function setAutoComplete(s) { var elem = $("#searchTextBox"); elem.autocomplete({ minLength: 2, source: function (request, response) { $.ajax({ type: "POST", url: currentHost() + "WebServices/Common.asmx/GetSearchComplete", data: "{searchTerm:'" + elem.val() + "'}", contentType: "application/json; charset=utf-8", success: function (msg) { if (msg.d != "") response($.parseJSON(msg.d)); else response('') } }); }, select: function (event, ui) { elem.addClass('ui-autocomplete-loading'); window.location.href = ui.item.linkUrl; elem.selected = ui.item; return false; }, dataType: "json" }) .data("autocomplete")._renderItem = function (ul, item) { return $("<li></li>") .data("item.autocomplete", item) .append("<a href='" + item.linkUrl + "'><ul class='search-complete'><li class='pict'><img src='" + item.pictUrl + "'/></li><li class='name'>" + item.label + "</li></ul></a>") .appendTo(ul); }; elem.keydown(function (e) { if (e.keyCode == 13) { if (typeof (elem.selected) == 'undefined') { elem.addClass('ui-autocomplete-loading'); SearchClick(); //     e.preventDefault(); } } }); }
      
      







Sphinxへの移行から、次のパイを入手しました。

  1. 人間の検索
  2. 検索ページの生成-クライアント上
  3. データベースのオフロード


Sphinxを使用する前のページ生成時間を正確に測定することはできません。喜びのために前に起こったことを書き留めておくことを忘れていたからです。 しかし、データベースには2万以上の製品があり、すべてが非常に迅速に機能します(1秒未満の生成)。



製品インデックスを最新の状態に保つため(およびデータベースに関連する)、10分ごとに更新されるデルタインデックスを使用します。 それまでの間、メインインデックスは30分ごとに5秒で完全に再構築されます。 MS SQLのアンロードは、サプライヤが価格リストをインポートするときに発生する長いクエリの実行中にテーブルロックを回避するのに役立ちました。



PS彼はhabraeffectを生き残れないので、彼はプロジェクトへのリンクをきれいにしたようです。



All Articles