7

コンピューター サイエンスの勉強中に、Prolog のようないくつかの関数型言語に出くわしましたが、現在は C#、Ruby JavaScript、Java などの命令型言語のみを過去 10 年間行っています。現在、私はオンライン ショップ用の全文検索エンジンを作成しており、すでに「必須の方法」にかなり進んでいます。しかし、Clojure の Haskell のようないくつかの関数型言語に出くわしたことで、関数型パラダイムの方がはるかによく適合し、命令型の方法はこの仕事に適したツールではないことが明らかになりました。

そのため、約 1,000 万件のレコードの全文索引があります。各レコードには、基本的に単語の出現が、そのレコードの ID とテキスト位置とともに含まれています。

ユーザーが検索文字列を入力すると、式ツリーに解析されます。たとえば、検索文字列「transformer 100 W」は次のような結果になります

AND('transformer%', OR(NEAR('100', 'W'), NEAR('100', 'watts'), '100W', '0.1kW'))

ここでは余分な「知性」が働いていますが、それはこの質問には関係ありません。

次に、式ツリーが再帰的に評価され、.NET-DataTables の形式で最大 100,000 行を返す可能性のある 2 つの SQL クエリが生成されます。これらはセットまたは辞書に読み込まれ、検索式全体に一致するすべての結果を見つけるために、述語に応じて共通部分と結合が適用されます。NEAR 評価では、見つかったオカレンスの位置インデックスも比較されます。しかし、これはすべて、多くの for ループを使用して命令的に行われます。

さらに、見つかった単語の出現のスコアを合計するランキング機能があります。接頭辞として、またはあいまい一致 (データベース サーバーによって行われる) でのみ検出される単語は、完全一致よりも低いスコアを取得します。

結果ページでこれらの単語を強調表示するために、結果の項目ごとに、一致したすべての単語の出現のリストも取得する必要があります。

したがって、大まかに評価アルゴリズムは次のような関数です

expression tree, full text index -> 
resulting items that match the expressin tree, 
each with a ranking sum 
and a list of all found word occurrences for this item

ここでは大まかな概要を説明しているだけですが、十分にイメージしていただければ幸いです。

「現実世界」の制約:

  • アプリケーション全体 (これまで) は C# で記述されているため、.NET との簡単な統合が最も重要です。
  • データのロードは .NET-DataTables に読み込まれ、評価および変換する必要があります。結果は、.NET 型 (辞書、セット、配列など) に含まれている必要があります。
  • パフォーマンスは非常に重要です。現在、私のアルゴリズムは、検索に 2 秒かかることがよくあります (SQL はカウントしません)。これは問題ありませんが、改善する必要があります。私たちのサーバーには 16 個のプロセッサがあるため、並列処理は大歓迎です。1 秒あたり約 1 件の検索要求があり、現在の実装はシングル スレッドであるため、プロセッサ時間はまだ利用可能です。
  • 言語 (およびコンパイラ) は成熟している必要があります。

私は .NET を使い続ける必要があるので、.NET 用の Clojure-CLR、F#、および Scala を調べていました。

私は Clojure の概念がとても気に入っていますが、今のところ、それが仕事次第かどうかは判断できません。F# について読むと、複雑な気持ちになりました。与えられたタスクに対して、より「純粋な」数学的アプローチを採用する傾向があるのに対し、F# はほぼすべてのことを実行できるように思われるからです。しかし、おそらくそれはF#でも可能であり、私はまだそれを認識していません. 私はまだ Scala について深く掘り下げていませんが、十分に確立されているようです。

どんな洞察も大歓迎です!

4

2 に答える 2

15
  • アプリケーション全体 (これまで) は C# で記述されているため、.NET との簡単な統合が最も重要です。
  • データのロードは .NET-DataTables に読み込まれ、評価および変換する必要があります。結果は、.NET 型 (辞書、セット、配列など) に含まれている必要があります。

F# は優れた選択肢です。F# は Visual Studio の第一級言語であるため、C# との相互運用性は非常に優れています。

  • パフォーマンスは非常に重要です。現在、私のアルゴリズムは検索に 2 秒かかることがよくあります (SQL はカウントしません)。これは問題ありませんが、改善する必要があります。私たちのサーバーには 16 個のプロセッサがあるため、並列処理は大歓迎です。1 秒あたり約 1 件の検索要求があり、現在の実装はシングル スレッドであるため、プロセッサ時間はまだ利用可能です。

機能優先で不変の実装から始めると仮定すると、アプリの並列化は簡単になるはずです。さらに、非同期ワークフローは、あなたのような IO バウンドのアプリケーションにとってはありがたいことです。

  • 言語 (およびコンパイラ) は成熟している必要があります。

F# を Clojure や JVM 上の Scala と比較することはしませんが、F# は .NET 上の Clojure CLR や Scala よりもはるかに成熟しています。F# を選択すると、Microsoft からの長期的なコミットメントと、成長を続ける F# コミュニティからの支援を受けることができます。

ユーザーが検索文字列を入力すると、式ツリーに解析されます。

判別共用体を使用して式ツリーを表すことができます。F# 3.0 でのクエリ式の導入により、ロジックを簡単に SQL クエリに変換できます。ドメインに同様のクエリ言語を定義することで、さらにプッシュすることもできます.

F# について読むと、複雑な気持ちになりました。与えられたタスクに対して、より「純粋な」数学的アプローチをする傾向があるのに対し、F# はほぼすべてのことを実行できるように思われるからです。しかし、おそらくそれはF#でも可能であり、私はまだそれを認識していません.

F# 3.0 では、タイプ プロバイダーが導入され、ユーザーが非構造化データにタイプ セーフな方法でアクセスできるようになりました。詳細については、この「F# 3.0 - Information Rich Programming」ビデオをご覧ください。F# をデータ マイニング用のプログラミング言語として使用したい場合は、関連する質問をしたところ、かなり良い回答が得られました

とはいえ、F# に対する最初の感覚は正しくないかもしれません。私の経験から、いつでも機能的で不変な面にできるだけ近づけることができます。興味深いアプリケーションが既にあることを考えると、F# が目的に適した言語であるかどうかを確認するために手を動かしてみることをお勧めします。

アップデート:

このアイデアを示す F# プロトタイプを次に示します。

/// You start by modeling your domain with a set of types.
/// FullText is a sequence of Records, which is processed on demand.
type Word = string
and Freq = int
and Record = {Occurrences: (Word * Freq) list; Id: string}
and FullText = Record seq

/// Model your expression tree by following the grammar closely.
type Expression =
    | Occur of Word
    | Near of Word * Word
    | And of Expression * Expression 
    | Or of Expression * Expression

/// Find wether a word w occurs in the occurrence list.
let occur w {Occurrences = xs} = xs |> Seq.map fst |> Seq.exists ((=) w)

/// Check whether two words are near each other.
/// Note that type annotation is only needed for the stub implementation.
let near (w1: Word) (w2: Word) (r: Record): bool = failwith "Not implemented yet"

/// Evaluate an expression tree.
/// The code is succinct and clear thanks to pattern matching. 
let rec eval expr r = 
    match expr with
    | Occur w -> occur w r
    | Near(w1, w2) -> near w1 w2 r
    | And(e1, e2) -> eval e1 r && eval e2 r
    | Or(e1, e2) -> eval e1 r || eval e2 r

/// Utility function which returns second element in a 3-tuple
let inline snd3 (_, x, _) = x

/// Get the rank of the record by adding up frequencies on the whole database.
let rank (r: Record) (ft: FullText): Freq = failwith "Not implemented yet"

/// Retrieve all records which match the expression tree.
let retrieve expr fullText =
    fullText |> Seq.filter (eval expr)
             |> Seq.map (fun r -> r, rank r fullText, r.Occurrences)
             |> Seq.sortBy snd3

/// An example query
let query = 
    And (Occur "transformer%", 
         Or (Or (Near ("100", "W"), Near ("100", "watts")), 
             Or (Occur "100W", Occur "0.1kW")))
于 2012-11-05T14:30:01.117 に答える
7

LINQ をオプションとして検討していない理由が知りたいです。すべての基準を満たしているようです。私は Scala の経験がないので、それについてコメントすることはできません。

  • アプリケーション全体 (これまで) は C# で記述されているため、.NET との簡単な統合が最も重要です。
  • データのロードは .NET-DataTables に読み込まれ、評価および変換する必要があります。結果は、.NET 型 (辞書、セット、配列など) に含まれている必要があります。

ここでは、LINQ > F# > Clojure-CLR です。すべてが既に C# にある場合は、LINQ が最も簡単に統合できます。インテリセンスや関数定義ナビゲーションなどの Visual Studio のサポートは、C# のみのプログラムではるかに優れているようです。C# から Clojure を呼び出すのは恐ろしいことです。理論的には問題なく動作するはずですが、実際には、期待どおりに動作しない理由を理解するために何週間も費やす覚悟をしてください。それは本当にトップレベルのものになるように設計されています。Clojure から C# を呼び出す場合、逆方向に進むことは、Clojure-CLR 開発者の優先順位リストの上位にはありません。基本的なサポートがありますが、得られるものは得られます。

  • パフォーマンスは非常に重要です。現在、私のアルゴリズムは検索に 2 秒かかることがよくあります (SQL はカウントしません)。これは問題ありませんが、改善する必要があります。私たちのサーバーには 16 個のプロセッサがあるため、並列処理は大歓迎です。1 秒あたり約 1 件の検索要求があり、現在の実装はシングル スレッドであるため、プロセッサ時間はまだ利用可能です。

LINQ ~= F# > Clojure. ほとんどの慣用的に記述されたアルゴリズムでは、LINQ のパフォーマンスが F# よりもわずかに優れていることが示されていることを他の場所で読んだことがありますが、それらは問題にならないほど十分に近いものです。PLINQ を使用すると、並列処理が容易になります。Clojure-CLR の起動時間は非常に遅く、実行時のオーバーヘッドも速度を低下させます。

  • 言語 (およびコンパイラ) は成熟している必要があります。

LINQ >= F# > Clojure. F# がまったく未熟であるとは言いませんが、Visual Studio のサポートは少し遅れており、世界には F# よりも LINQ に基づいたプロダクション コード (およびスタック オーバーフローの回答がはるかに多い) が存在します。

F# について読むと、複雑な気持ちになりました。与えられたタスクに対して、より「純粋な」数学的アプローチを採用する傾向があるのに対し、F# はほぼすべてのことを実行できるように思われるからです。しかし、おそらくそれはF#でも可能であり、私はまだそれを認識していません.

Haskell のように純粋で純粋な言語はありませんが、非純粋なコードを書くことがどれほど難しいかという点で、LINQ > Clojure > F# > Scala にランク付けします。LINQ は、不純なメソッドを呼び出すことによってのみ不純にすることができます。Clojure には ref と atom があり、F# は何でも変更可能に指定できます。Scala (私の理解によると) は実際には、機能的な機能がボルトで固定された単なる Java です。

F# と Scala の両方が備えている機能的な機能は、パターン マッチングの言語サポートです。C# では、機能的に物事を行うためにある種の継承階層または b?x:y 演算子のチェーンが必要な場合 (または、非機能的なアプローチで問題がない場合は if/else)、パターン マッチングは異なる条件付き操作を行います。生データ型のバリエーションははるかに簡潔です。これは、完全一致 vs 接頭辞 vs あいまい一致のランキングの計算に役立つ可能性がありvar alg = x.match == exact ? alg1 : x.match == prefix ? alg2 : alg3ますが、この単純なケースでは、C# の ab?x:y チェーンは完全に判読可能です。言語統合パターン マッチングがより複雑になるのは、マッチングがより複雑になるときです。貴重。

興味深いことに、F# が LINQ よりも有用であることが証明されるツールキットの 1 つの側面は、クエリではなく、LINQ の名前自体が処理できることを示していると思いますが、検索文字列を式ツリーに解析します。 これは、関数型言語とパターン マッチングが非常に優れている分野の 1 つであり、FsLex や FsYacc などのツールを追加すると、大きな有利なスタートを切ることができます。

そうは言っても、決定はあなたがどこに行きたいかによると思います。検索アルゴリズムをクリーンアップして完了したい場合は、LINQ アプローチをお勧めします。しかし、プログラム全体を少しずつ機能指向のスタイルにしたい場合 (そして、あなたの会社は、あなたがコミットしている時間に対して喜んでお金を払ってくれるでしょう)、F# を見てください。オプション。いずれにせよ、最初に LINQ オプションを実行します。その方がより簡単であり、そのパスを開始すると、F# がより機能的に慣用的になるようにガイドするのに役立ちます。

簡単に言うと、これがあなたが望むものです。Near および Equal フェッチャーの関数と、GetRank および GetStrings 関数を入力し、以下を使用します。

static IEnumerable<Record> FetchRecords(this Tree tree) {
    return tree.Op == "OR"    ? tree.Args.SelectMany(FetchRecords).Distinct() :
           tree.Op == "AND"   ? tree.Args.Select(FetchRecords).Aggregate((intersect, current) => intersect.Intersect(current)) :
           tree.Op == "NEAR"  ? FetchValsNear(tree.Args[0].Op, tree.Args[1].Op) :
                                FetchValsEqual(tree.Op);
}

static IEnumerable<Record> FetchValsEqual(string s) {
    throw new NotImplementedException();
}

static IEnumerable<Record> FetchValsNear(string s1, string s2) {
    throw new NotImplementedException();
}

static IEnumerable<Tuple<Record, double, string[]>> OrderByRank(this IEnumerable<Record> vals) {
    return from val in vals
           let rank = GetRank(val)
           orderby rank
           let strings = GetStringsIn(val)
           select Tuple.Create(val, rank, strings);
}

static string[] GetStringsIn(Record val) {
    throw new NotImplementedException();
}

static double GetRank(Record val) {
    throw new NotImplementedException();
}

class Tree {
    public string Op;
    public Tree[] Args;
}

struct Record {/*your record here--use struct so Distinct and Intersect above work naturally (or use class and override Equals)*/}

このような:

foreach (var tuple in myTree.FetchRecords().AsParallel().OrderByRank().Take(30)) {
    // add to datagrid or whatever
}

これにより、単純な並列化と遅延の両方が得られるため、GetStringsIn取得したレコード (この場合は上位 30 件) に対してのみ関数が実行されます。(セレクターは、こちらの例ANDのいずれかを使用して簡略化できることに注意してください)。IntersectAll

于 2012-11-05T15:44:10.373 に答える