web-dev-qa-db-ja.com

APIのクエリ文字列で一般的なフィルタリング演算子を設計するにはどうすればよいですか?

ユーザー定義可能なコンテンツとスキーマを備えた汎用APIを構築しています。 API応答にフィルタリングロジックを追加して、ユーザーがAPIに保存した特定のオブジェクトをクエリできるようにします。たとえば、ユーザーがイベントオブジェクトを保存している場合、次のようなフィルター処理を実行できます。

  • Array containsproperties.categoriesEngineeringを含むかどうか
  • より大きいproperties.created_at2016-10-02より古いかどうか
  • 等しくないproperties.address.cityWashingtonではないかどうか
  • Equalproperties.nameMeetupかどうか
  • 等.

API応答のクエリ文字列へのフィルタリングを設計しようとしていて、いくつかのオプションを考え出しましたが、どの構文が最適であるかわかりません...


1.ネストされたキーとしての演算子

/events?properties.name=Harry&properties.address.city.neq=Washington

この例では、ネストされたオブジェクトを使用して演算子を指定しています(図のようにneqなど)。これは非常にシンプルで読みやすいという点でいいです。

ただし、ユーザーがイベントのプロパティを定義できる場合は、通常の等号演算子を使用するaddress.city.neqという名前のプロパティと、等しくない演算子を使用するaddress.cityという名前のプロパティが競合する可能性があるという問題が発生します。

例: Stripe's API


2.キーサフィックスとしての演算子

/events?properties.name=Harry&properties.address.city+neq=Washington

この例は、最初の例と似ていますが、+ではなく、.区切り文字(スペースに相当)を使用して、ドメイン内のキーにスペースを含めることができないため、混乱が生じない点が異なります。

一つの欠点は、それがより明確であると解釈されるかもしれないのでそれは議論の余地がありますが、それは読むのが少し難しいです。もう1つは、解析が少し難しいが、それほど多くないことです。


3.値の接頭辞としての演算子

/events?properties.name=Harry&properties.address.city=neq:Washington

この例は、演算子の構文をキーではなくパラメーターの値に移動することを除いて、前の例と非常に似ています。これには、クエリ文字列の解析の複雑さを少し解消するという利点があります。

しかし、これはリテラル文字列neq:Washingtonをチェックする等号演算子と文字列Washingtonをチェックする不等号演算子を区別できなくなるという犠牲を伴います。

例: SparkpayのAPI


4.カスタムフィルターパラメーター

/events?filter=properties.name==Harry;properties.address.city!=Washington

この例では、単一のトップレベルのクエリパラメータfilterを使用して、その下にあるすべてのフィルタリングロジックに名前空間を付けています。これは、トップレベルのネームスペースの衝突を心配する必要がないという点で素晴らしいです。 (私の場合、すべてのカスタムはproperties.の下にネストされているので、これは最初から問題ではありません。)

しかし、これは基本的な等式フィルタリングを実行するときに入力するのが難しいクエリ文字列を使用するという犠牲を伴います。そのため、ほとんどの場合、ドキュメントを確認する必要があります。また、演算子の記号に依存すると、「near」、「within」、「contains」などの自明ではない操作が混乱する可能性があります。

例: Google AnalyticsのAPI


5.カスタム詳細フィルターパラメーター

/events?filter=properties.name eq Harry; properties.address.city neq Washington

この例では、前のものと同様の最上位のfilterパラメーターを使用していますが、演算子を記号で定義するのではなく、Wordで演算子をスペルアウトし、その間にスペースを入れています。これは少し読みやすいかもしれません。

しかし、これにはURLが長くなり、エンコードする必要のあるスペースが多くなるという犠牲が伴います。

例: ODataのAPI


6.オブジェクトフィルターパラメーター

/events?filter[1][key]=properties.name&filter[1][eq]=Harry&filter[2][key]=properties.address.city&filter[2][neq]=Washington

この例でもトップレベルのfilterパラメーターを使用していますが、プログラミングを模倣した完全なカスタム構文を作成する代わりに、より標準的なクエリ文字列構文を使用してフィルターのオブジェクト定義を構築しています。これには、もう少し「標準」をもたらすという利点があります。

ただし、入力が非常に冗長になり、解析が難しくなります。

MagentoのAPI


これらすべての例、または別のアプローチを考えると、どの構文が最適ですか?理想的には、クエリパラメータを簡単に作成できるので、URLバーで遊んでみることはできますが、将来の相互運用性に問題が生じることはありません。

#2に傾いていますが、それは読みやすいようですが、他のスキームのいくつかの欠点もありません。

19

「どちらが最適か」という質問には答えないかもしれませんが、少なくともいくつかの洞察と、検討すべき他の例を提供できます。

最初に、「ユーザー定義可能なコンテンツとスキーマを備えた汎用API」について話しています。

これは solr / elasticsearch のように聞こえます。どちらも Apache Lucene の高レベルラッパーであり、基本的にドキュメントのインデックス付けと集計を行います。

それらの2つは、残りのAPIに対してまったく異なるアプローチをとり、たまたま私は両方で作業しました。

Elasticsearch:

彼らはJSONベースのクエリDSLを作成しましたが、現在は次のようになっています。

GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }}, 
        { "match": { "content": "Elasticsearch" }}  
      ],
      "filter": [ 
        { "term":  { "status": "published" }}, 
        { "range": { "publish_date": { "gte": "2015-01-01" }}} 
      ]
    }
  }
}

現在の doc から取得。実際にデータを[〜#〜] get [〜#〜]に入れることができることに驚いていました...実際には、以前は実際より良く見えますバージョンそれははるかに 階層的 でした。

私の個人的な経験から、このDSLは強力でしたが、流暢に習得して使用することはかなり困難でした(特に古いバージョン)。実際に結果を得るには、URLを操作するだけでは不十分です。多くのクライアントが[〜#〜] get [〜#〜]リクエストのデータをサポートしていないという事実から始まります。

SOLR:

彼らはすべてを基本的に次のように見えるクエリパラメータに入れます( doc から取得):

q=*:*&fq={!cache=false cost=5}inStock:true&fq={!frange l=1 u=4 cache=false cost=50}sqrt(popularity)

それでの作業はより簡単でした。しかし、それは私の個人的な好みです。


今私の経験について。これらの2つの上の別のレイヤーを実装し、アプローチ番号#4を採用しました。実際、私は#4#5を同時にサポートする必要があります。どうして?あなたが選んだものは何でも文句を言うでしょう、そしてあなたはとにかくあなた自身の「マイクロDSL」を持っているので、あなたはあなたのキーワードのためにいくつかのより多くのエイリアスをサポートすることもできます。

なぜ#2ではないのですか?内部に単一のフィルターパラメーターとクエリがあると、DSLを完全に制御できます。リソースを作成してから半年後、「シンプルな」機能リクエストがありました-論理ORと括弧()。クエリパラメータは基本的にAND操作のリストであり、city=London OR age>25のような論理ORは実際には適合しません。一方、括弧はDSL構造にネストを導入しましたが、これはフラットなクエリ文字列構造でも問題になります。

まあ、それらは私たちが偶然見つけた問題でした、あなたのケースは異なるかもしれません。しかし、このAPIからの将来の期待はどのようなものになるかについては、検討する価値があります。

6
James Cube

#4

Google AnalyticsフィルターAPIがどのように見え、使いやすく、クライアントの観点から理解しやすいのが好きです。

たとえば、URLエンコードされたフォームを使用します。

  • 等しい%3D%3Dfilters=ga:timeOnPage%3D%3D10
  • 等しくない!%3Dfilters=ga:timeOnPage!%3D10

ドキュメントを確認する必要がありますが、それでも独自の利点があります。ユーザーがこれに慣れることができると思ったら、それを試してください。


#2

演算子をキーサフィックスとして使用することも(要件に応じて)良いアイデアのように思えます。

ただし、spaceとして解析されないように、+記号をエンコードすることをお勧めします。また、前述のように解析が少し難しいかもしれませんが、このパーサー用のカスタムパーサーを作成できると思います。 this Gistを jlong​​ で偶然見つけました。おそらく、パーサーを作成すると便利でしょう。

1

あなたも試すことができます Spring Expression Language(SpEL)

必要なのは、ドキュメント内の前述の形式に固執することだけです。SpELエンジンがクエリを解析し、指定されたオブジェクトで実行します。オブジェクトのリストをフィルタリングするという要件と同様に、クエリを次のように書くことができます。

properties.address.city == 'Washington' and properties.name == 'Harry'

必要なあらゆる種類の関係演算子と論理演算子をサポートしています。残りのAPIは、このクエリをフィルター文字列として受け取り、SpELエンジンに渡してオブジェクトで実行することができます。

利点:読みやすく、書き込みが簡単で、実行が適切に行われます。

したがって、URLは次のようになります。

/events?filter="properties.address.city == 'Washington' and properties.name == 'Harry'"

Org.springframework:spring-core:4.3.4.RELEASEを使用したサンプルコード:

関心のある主な機能:

    /**
     * Filter the list of objects based on the given query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

ヘルパークラスとその他の興味のないコードを含む完全な例:

import Java.util.Arrays;
import Java.util.List;
import Java.util.stream.Collectors;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class SpELTest {

    public static void main(String[] args) {
        String query = "address.city == 'Washington' and name == 'Harry'";

        Event event1 = new Event(new Address("Washington"), "Harry");
        Event event2 = new Event(new Address("XYZ"), "Harry");

        List<Event> events = Arrays.asList(event1, event2);

        List<Event> filteredEvents = filter(query, events);

        System.out.println(filteredEvents.size()); // 1
    }

    /**
     * Filter the list of objects based on the query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

    public static class Event {
        private Address address;
        private String name;

        public Event(Address address, String name) {
            this.address = address;
            this.name = name;
        }

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

    }

    public static class Address {
        private String city;

        public Address(String city) {
            this.city = city;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }

    }
}
1
Sourabh

私はアプローチを比較することにしました#1 /#2(1)と(2)、そして(1)が好ましいと結論付けました(少なくとも、Javaサーバー側)。

一部のパラメータaは10または20でなければなりません。この場合のURLクエリは、(1)の場合は_?a.eq=10&a.eq=20_、(2)の場合は_?a=eq:10&a=eq:20_のようになります。 Java HttpServletRequest#getParameterMap()では、次の値が返されます:(1)の場合は_{ a.eq: [10, 20] }_、(2)の場合は_{ a: [eq:10, eq:20] }_。後で、返されたマップを変換する必要があります、たとえば、SQL where句を使用すると、(1)と(2)の両方で_where a = 10 or a = 20_を取得する必要があります。

_1) ?a=eq:10&a=eq:20 -> { a: [eq:10, eq:20] } -> where a = 10 or a = 20
2) ?a.eq=10&a.eq=20 -> { a.eq: [10, 20] }    -> where a = 10 or a = 20
_

したがって、次のルールを取得しました:同じ名前のURLクエリ2つのパラメーターを渡す場合、SQLでORオペランドを使用する必要があります

しかし、別のケースを想定してみましょう。パラメータaは10より大きく20未満でなければなりません。上記のルールを適用すると、次の変換が行われます。

_1) ?a.gt=10&a.ls=20 -> { a.gt: 10, a.lt: 20 } -> where a > 10 and a < 20
2) ?a=gt:10&a=ls:20 -> { a: [gt.10, lt.20] }  -> where a > 10 or(?!) a < 20
_

ご覧のとおり、(1)にはdifferentという名前の2つのパラメーター、_a.gt_と_a.ls_があります。つまり、SQLクエリにはANDオペランドがあります。ただし、(2)の場合も同じ名前があり、ORオペランドを使用してSQLに変換する必要があります。

つまり、(2)では、#getParameterMap()を使用する代わりに、URLクエリを直接解析して、繰り返されるパラメーター名を分析する必要があります。

0
pto3

私はこれが古い学校であることを知っていますが、一種の演算子の過負荷はどうですか?

クエリの解析がかなり難しくなります(標準のCGIではありません)が、SQL WHERE句の内容に似ています。

/events?properties.name=Harry&properties.address.city+neq=ワシントン

なるだろう

/events?properties.name=='Harry'&&properties.address.city!='Washington'||properties.name=='Jack'&&properties.address.city!=('Paris','New Orleans ')

括弧はリストを開始します。文字列を引用符で囲むと、解析が簡単になります。

したがって、上記のクエリは、ワシントンにないハリーのイベント、またはパリやニューオーリンズにないジャックのイベントに対するものです。

実装するのは大変な作業です...そして、これらのクエリを実行するためのデータベースの最適化は悪夢ですが、単純で強力なクエリ言語を探している場合は、SQLを模倣するだけです:)

-k

0
schuttek