필터링 (Filtering)
필터링 (Filtering)
원문: Qdrant 공식 문서 - Filtering (Qdrant Concepts)
이 문서는 Qdrant 공식 문서를 한국어로 옮기고, 옆에서 설명하는 강사 목소리로 다시 쓴 해설입니다. 코드·명령어·버전·조건 같은 기술 용어는 원문 그대로 보존했어요.
Qdrant는 포인트를 검색하거나 조회할 때 조건을 걸 수 있어요. 예를 들어 포인트의 payload와 id 양쪽에 조건을 걸 수 있죠.
이렇게 추가 조건을 거는 건, 객체의 모든 특징을 embedding 안에 다 담을 수 없을 때 특히 중요해요. 재고가 있는지, 사용자 위치가 어디인지, 원하는 가격대가 어딘지 같은 건 embedding으로 표현하기 어렵거든요. 이런 비즈니스 요구를 필터로 해결해요.
필터링 절 (Filtering clauses)
Qdrant는 조건들을 절(clause) 단위로 묶을 수 있어요. 절은 OR, AND, NOT 같은 논리 연산이고요. 절끼리는 재귀적으로 중첩할 수 있어서, 사실상 임의의 부울(boolean) 표현식을 재현할 수 있어요.
Qdrant가 지원하는 절들을 하나씩 살펴볼게요. 설명에 쓸 예시 데이터로, 이런 payload를 가진 포인트 집합이 있다고 가정해요.
[
{ "id": 1, "city": "London", "color": "green" },
{ "id": 2, "city": "London", "color": "red" },
{ "id": 3, "city": "London", "color": "blue" },
{ "id": 4, "city": "Berlin", "color": "red" },
{ "id": 5, "city": "Moscow", "color": "green" },
{ "id": 6, "city": "Moscow", "color": "blue" }
]
Must
must를 쓰면, 안에 나열한 모든 조건이 만족돼야 그 절이 true가 돼요. 이런 점에서 must는 AND 연산자와 같아요.
예시를 볼게요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must": [
{ "key": "city", "match": { "value": "London" } },
{ "key": "color", "match": { "value": "red" } }
]
}
...
}
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must=[
models.FieldCondition(
key="city",
match=models.MatchValue(value="London"),
),
models.FieldCondition(
key="color",
match=models.MatchValue(value="red"),
),
]
),
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
client.scroll("{collection_name}", {
filter: {
must: [
{
key: "city",
match: { value: "London" },
},
{
key: "color",
match: { value: "red" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must([
Condition::matches("city", "london".to_string()),
Condition::matches("color", "red".to_string()),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addAllMust(
List.of(matchKeyword("city", "London"), matchKeyword("color", "red")))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
// & operator combines two conditions in an AND conjunction(must)
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: MatchKeyword("city", "London") & MatchKeyword("color", "red")
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("city", "London"),
qdrant.NewMatch("color", "red"),
},
},
})
이 필터를 통과하는 포인트는 이렇게 돼요.
[{ "id": 2, "city": "London", "color": "red" }]
city가 London이면서 동시에 color가 red인 포인트는 id 2 하나뿐이죠.
Should
should를 쓰면, 안에 나열한 조건 중 하나라도 만족하면 그 절이 true가 돼요. 이런 점에서 should는 OR 연산자와 같아요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"should": [
{ "key": "city", "match": { "value": "London" } },
{ "key": "color", "match": { "value": "red" } }
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
should=[
models.FieldCondition(
key="city",
match=models.MatchValue(value="London"),
),
models.FieldCondition(
key="color",
match=models.MatchValue(value="red"),
),
]
),
)
client.scroll("{collection_name}", {
filter: {
should: [
{
key: "city",
match: { value: "London" },
},
{
key: "color",
match: { value: "red" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::should([
Condition::matches("city", "london".to_string()),
Condition::matches("color", "red".to_string()),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addAllShould(
List.of(matchKeyword("city", "London"), matchKeyword("color", "red")))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
// | operator combines two conditions in an OR disjunction(should)
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: MatchKeyword("city", "London") | MatchKeyword("color", "red")
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Should: []*qdrant.Condition{
qdrant.NewMatch("city", "London"),
qdrant.NewMatch("color", "red"),
},
},
})
이번에는 London이거나 red인 포인트가 모두 걸려요.
[
{ "id": 1, "city": "London", "color": "green" },
{ "id": 2, "city": "London", "color": "red" },
{ "id": 3, "city": "London", "color": "blue" },
{ "id": 4, "city": "Berlin", "color": "red" }
]
Must Not
must_not을 쓰면, 안에 나열한 조건 중 아무것도 만족하지 않아야 그 절이 true가 돼요. 표현식으로는 (NOT A) AND (NOT B) AND (NOT C)와 같아요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must_not": [
{ "key": "city", "match": { "value": "London" } },
{ "key": "color", "match": { "value": "red" } }
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must_not=[
models.FieldCondition(key="city", match=models.MatchValue(value="London")),
models.FieldCondition(key="color", match=models.MatchValue(value="red")),
]
),
)
client.scroll("{collection_name}", {
filter: {
must_not: [
{
key: "city",
match: { value: "London" },
},
{
key: "color",
match: { value: "red" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must_not([
Condition::matches("city", "london".to_string()),
Condition::matches("color", "red".to_string()),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addAllMustNot(
List.of(matchKeyword("city", "London"), matchKeyword("color", "red")))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
// The ! operator negates the condition(must not)
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: !(MatchKeyword("city", "London") & MatchKeyword("color", "red"))
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
MustNot: []*qdrant.Condition{
qdrant.NewMatch("city", "London"),
qdrant.NewMatch("color", "red"),
},
},
})
London도 아니고 red도 아닌 포인트만 남아요.
[
{ "id": 5, "city": "Moscow", "color": "green" },
{ "id": 6, "city": "Moscow", "color": "blue" }
]
절 조합 (Clauses combination)
여러 절을 동시에 쓰는 것도 가능해요. 예를 들어 must와 must_not을 함께 쓰면, London이면서 red는 아닌 포인트를 구할 수 있어요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must": [
{ "key": "city", "match": { "value": "London" } }
],
"must_not": [
{ "key": "color", "match": { "value": "red" } }
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must=[
models.FieldCondition(key="city", match=models.MatchValue(value="London")),
],
must_not=[
models.FieldCondition(key="color", match=models.MatchValue(value="red")),
],
),
)
client.scroll("{collection_name}", {
filter: {
must: [
{
key: "city",
match: { value: "London" },
},
],
must_not: [
{
key: "color",
match: { value: "red" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter {
must: vec![Condition::matches("city", "London".to_string())],
must_not: vec![Condition::matches("color", "red".to_string())],
..Default::default()
}),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addMust(matchKeyword("city", "London"))
.addMustNot(matchKeyword("color", "red"))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: MatchKeyword("city", "London") & !MatchKeyword("color", "red")
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("city", "London"),
},
MustNot: []*qdrant.Condition{
qdrant.NewMatch("color", "red"),
},
},
})
필터를 통과하는 포인트는 id 1과 id 3이에요.
[
{ "id": 1, "city": "London", "color": "green" },
{ "id": 3, "city": "London", "color": "blue" }
]
이 경우 조건들은 AND로 결합돼요. 여기서 더 나아가 절을 재귀적으로 중첩할 수도 있어요. 아래 예시는 must_not 안에 must를 넣어서, "city가 London이면서 color가 red"인 조합을 통째로 부정하는 필터예요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must_not": [
{
"must": [
{ "key": "city", "match": { "value": "London" } },
{ "key": "color", "match": { "value": "red" } }
]
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must_not=[
models.Filter(
must=[
models.FieldCondition(
key="city", match=models.MatchValue(value="London")
),
models.FieldCondition(
key="color", match=models.MatchValue(value="red")
),
],
),
],
),
)
client.scroll("{collection_name}", {
filter: {
must_not: [
{
must: [
{
key: "city",
match: { value: "London" },
},
{
key: "color",
match: { value: "red" },
},
],
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must_not([Filter::must(
[
Condition::matches("city", "London".to_string()),
Condition::matches("color", "red".to_string()),
],
)
.into()])),
)
.await?;
import static io.qdrant.client.ConditionFactory.filter;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addMustNot(
filter(
Filter.newBuilder()
.addAllMust(
List.of(
matchKeyword("city", "London"),
matchKeyword("color", "red")))
.build()))
.build())
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: new Filter { MustNot = { MatchKeyword("city", "London") & MatchKeyword("color", "red") } }
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
MustNot: []*qdrant.Condition{
qdrant.NewFilterAsCondition(&qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("city", "London"),
qdrant.NewMatch("color", "red"),
},
}),
},
},
})
이 필터를 통과하는 포인트는 id 2만 빠진 나머지 전부예요.
[
{ "id": 1, "city": "London", "color": "green" },
{ "id": 3, "city": "London", "color": "blue" },
{ "id": 4, "city": "Berlin", "color": "red" },
{ "id": 5, "city": "Moscow", "color": "green" },
{ "id": 6, "city": "Moscow", "color": "blue" }
]
필터링 조건 (Filtering conditions)
payload에 담긴 값의 타입에 따라 적용할 수 있는 쿼리 종류가 달라져요. 이제 실제로 존재하는 조건(condition) 변형들과, 각각 어떤 타입의 데이터에 적용되는지 살펴볼게요.
Match
가장 단순한 조건으로, 저장된 값이 주어진 값과 같은지 확인해요. 여러 값이 저장돼 있다면 그중 하나라도 조건과 일치하면 돼요. keyword, integer, bool payload에 적용할 수 있어요.
{
"key": "color",
"match": {
"value": "red"
}
}
models.FieldCondition(
key="color",
match=models.MatchValue(value="red"),
)
{
key: 'color',
match: {value: 'red'}
}
Condition::matches("color", "red".to_string())
import static io.qdrant.client.ConditionFactory.matchKeyword;
matchKeyword("color", "red");
using static Qdrant.Client.Grpc.Conditions;
MatchKeyword("color", "red");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatch("color", "red")
다른 타입이라도 match 조건의 모양은 똑같아요. 타입만 달라질 뿐이에요. 정수 예시를 볼게요.
{
"key": "count",
"match": {
"value": 0
}
}
models.FieldCondition(
key="count",
match=models.MatchValue(value=0),
)
{
key: 'count',
match: {value: 0}
}
Condition::matches("count", 0)
import static io.qdrant.client.ConditionFactory.match;
match("count", 0);
using static Qdrant.Client.Grpc.Conditions;
Match("count", 0);
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchInt("count", 0)
Match Any
Available as of v1.1.0
저장된 값이 여러 값 중 하나라도 해당하는지 확인하고 싶을 때는 Match Any 조건을 써요. Match Any는 주어진 값들에 대한 논리적 OR처럼 동작해서, IN 연산자로도 설명할 수 있어요.
{
"key": "color",
"match": {
"any": ["black", "yellow"]
}
}
models.FieldCondition(
key="color",
match=models.MatchAny(any=["black", "yellow"]),
)
{
key: 'color',
match: {any: ['black', 'yellow']}
}
Condition::matches("color", vec!["black".to_string(), "yellow".to_string()])
import static io.qdrant.client.ConditionFactory.matchKeywords;
import java.util.List;
matchKeywords("color", List.of("black", "yellow"));
using static Qdrant.Client.Grpc.Conditions;
Match("color", ["black", "yellow"]);
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchKeywords("color", "black", "yellow")
이 예시에서는 저장된 값이 black 또는 yellow면 조건이 만족돼요. 만약 저장된 값 자체가 배열이라면, 배열 안에 주어진 값 중 하나라도 있으면 돼요. 예를 들어 저장된 값이 ["black", "green"]이면, "black"이 ["black", "yellow"]에 있으니 조건이 만족되는 거죠.
Match Except
Available as of v1.2.0
저장된 값이 여러 값 중 어느 것도 아닌지 확인하고 싶으면 Match Except 조건을 써요. Match Except는 주어진 값들에 대한 논리적 NOR처럼 동작해서, NOT IN 연산자로도 설명할 수 있어요.
{
"key": "color",
"match": {
"except": ["black", "yellow"]
}
}
models.FieldCondition(
key="color",
match=models.MatchExcept(**{"except": ["black", "yellow"]}),
)
{
key: 'color',
match: {except: ['black', 'yellow']}
}
use qdrant_client::qdrant::r#match::MatchValue;
Condition::matches(
"color",
!MatchValue::from(vec!["black".to_string(), "yellow".to_string()]),
)
import static io.qdrant.client.ConditionFactory.matchExceptKeywords;
import java.util.List;
matchExceptKeywords("color", List.of("black", "yellow"));
using static Qdrant.Client.Grpc.Conditions;
MatchExcept("color", ["black", "yellow"]);
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchExcept("color", "black", "yellow")
이 예시에서는 저장된 값이 black도 yellow도 아니면 조건이 만족돼요. 저장된 값이 배열이라면, 배열 안에 주어진 값에 해당하지 않는 값이 하나라도 있으면 돼요. 예를 들어 저장된 값이 ["black", "green"]이면, "green"이 "black"도 "yellow"도 아니니 조건이 만족되는 거죠.
중첩 키 (Nested key)
Available as of v1.1.0
payload는 임의의 JSON 객체다 보니, 중첩된 필드에 대해 필터링하고 싶은 경우가 많아요. 그래서 Qdrant는 Jq 프로젝트에서 쓰는 것과 비슷한 점 표기법(dot notation) 문법을 사용해요.
이런 payload를 가진 포인트 집합이 있다고 가정할게요.
[
{
"id": 1,
"country": {
"name": "Germany",
"cities": [
{
"name": "Berlin",
"population": 3.7,
"sightseeing": ["Brandenburg Gate", "Reichstag"]
},
{
"name": "Munich",
"population": 1.5,
"sightseeing": ["Marienplatz", "Olympiapark"]
}
]
}
},
{
"id": 2,
"country": {
"name": "Japan",
"cities": [
{
"name": "Tokyo",
"population": 9.3,
"sightseeing": ["Tokyo Tower", "Tokyo Skytree"]
},
{
"name": "Osaka",
"population": 2.7,
"sightseeing": ["Osaka Castle", "Universal Studios Japan"]
}
]
}
}
]
중첩 필드는 점 표기법으로 검색할 수 있어요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"should": [
{
"key": "country.name",
"match": {
"value": "Germany"
}
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
should=[
models.FieldCondition(
key="country.name", match=models.MatchValue(value="Germany")
),
],
),
)
client.scroll("{collection_name}", {
filter: {
should: [
{
key: "country.name",
match: { value: "Germany" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::should([
Condition::matches("country.name", "Germany".to_string()),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addShould(matchKeyword("country.name", "Germany"))
.build())
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(collectionName: "{collection_name}", filter: MatchKeyword("country.name", "Germany"));
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Should: []*qdrant.Condition{
qdrant.NewMatch("country.name", "Germany"),
},
},
})
배열 안의 값들도 [] 문법으로 투영해서 검색할 수 있어요. 예를 들어 "인구가 9.0 이상인 도시가 있는 나라"를 찾아볼게요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"should": [
{
"key": "country.cities[].population",
"range": {
"gte": 9.0,
}
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
should=[
models.FieldCondition(
key="country.cities[].population",
range=models.Range(
gt=None,
gte=9.0,
lt=None,
lte=None,
),
),
],
),
)
client.scroll("{collection_name}", {
filter: {
should: [
{
key: "country.cities[].population",
range: {
gt: null,
gte: 9.0,
lt: null,
lte: null,
},
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, Range, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::should([
Condition::range(
"country.cities[].population",
Range {
gte: Some(9.0),
..Default::default()
},
),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.range;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Common.Range;
import io.qdrant.client.grpc.Points.ScrollPoints;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addShould(
range(
"country.cities[].population",
Range.newBuilder().setGte(9.0).build()))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: Range("country.cities[].population", new Qdrant.Client.Grpc.Range { Gte = 9.0 })
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Should: []*qdrant.Condition{
qdrant.NewRange("country.cities[].population", &qdrant.Range{
Gte: qdrant.PtrOf(9.0),
}),
},
},
})
이 쿼리는 id 2 포인트만 반환해요. 인구가 9.0을 넘는 도시를 가진 나라는 일본뿐이니까요.
그리고 가장 마지막의 중첩 필드가 배열일 수도 있어요. "sightseeing(볼거리)에 Osaka Castle이 포함된 나라"를 찾아볼게요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"should": [
{
"key": "country.cities[].sightseeing",
"match": {
"value": "Osaka Castle"
}
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
should=[
models.FieldCondition(
key="country.cities[].sightseeing",
match=models.MatchValue(value="Osaka Castle"),
),
],
),
)
client.scroll("{collection_name}", {
filter: {
should: [
{
key: "country.cities[].sightseeing",
match: { value: "Osaka Castle" },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::should([
Condition::matches("country.cities[].sightseeing", "Osaka Castle".to_string()),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addShould(matchKeyword("country.cities[].sightseeing", "Germany"))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: MatchKeyword("country.cities[].sightseeing", "Germany")
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Should: []*qdrant.Condition{
qdrant.NewMatch("country.cities[].sightseeing", "Germany"),
},
},
})
이 쿼리도 id 2 포인트만 반환해요. sightseeing에 "Osaka Castle"이 포함된 도시가 있는 나라는 일본뿐이니까요. (참고로 원문에는 "Osaka castke"라는 오타가 있는데, 실제 값은 "Osaka Castle"이에요.)
중첩 객체 필터 (Nested object filter)
Available as of v1.2.0
기본적으로 조건들은 포인트의 전체 payload를 기준으로 판단돼요. 이게 언제 문제가 되는지 예를 들어볼게요. 이런 payload를 가진 두 포인트가 있다고 해요.
[
{
"id": 1,
"dinosaur": "t-rex",
"diet": [
{ "food": "leaves", "likes": false},
{ "food": "meat", "likes": true}
]
},
{
"id": 2,
"dinosaur": "diplodocus",
"diet": [
{ "food": "leaves", "likes": true},
{ "food": "meat", "likes": false}
]
}
]
아래 쿼리는 두 포인트 모두를 매칭할 거예요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must": [
{
"key": "diet[].food",
"match": {
"value": "meat"
}
},
{
"key": "diet[].likes",
"match": {
"value": true
}
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must=[
models.FieldCondition(
key="diet[].food", match=models.MatchValue(value="meat")
),
models.FieldCondition(
key="diet[].likes", match=models.MatchValue(value=True)
),
],
),
)
client.scroll("{collection_name}", {
filter: {
must: [
{
key: "diet[].food",
match: { value: "meat" },
},
{
key: "diet[].likes",
match: { value: true },
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must([
Condition::matches("diet[].food", "meat".to_string()),
Condition::matches("diet[].likes", true),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.match;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addAllMust(
List.of(matchKeyword("diet[].food", "meat"), match("diet[].likes", true)))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: MatchKeyword("diet[].food", "meat") & Match("diet[].likes", true)
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("diet[].food", "meat"),
qdrant.NewMatchBool("diet[].likes", true),
},
},
})
왜 두 포인트가 모두 매칭되는지 볼게요.
- "t-rex"는
diet[1].food에서 food=meat를,diet[1].likes에서 likes=true를 매칭해요. - "diplodocus"는
diet[1].food에서 food=meat를,diet[0].likes에서 likes=true를 매칭해요.
즉 조건들이 서로 다른 배열 요소에서 각각 만족돼도, 기본 필터는 포인트 전체를 보고 매칭한 걸로 판단하는 거예요. 이 예시에서는 id 1 포인트만, "같은 배열 요소 하나가 두 조건을 모두 만족"하는 경우로 다루고 싶다면? 그럴 때 **중첩 객체 필터(nested object filter)**가 필요해요.
중첩 객체 필터는 객체들의 배열을 서로 독립적으로 쿼리할 수 있게 해줘요. nested 조건 타입을 쓰는데, 이 조건은 집중할 payload 키와, 적용할 필터로 구성돼요. 키는 객체 배열을 가리켜야 하고, 대괄호 표기법을 쓰거나("data" 또는 "data[]") 쓰지 않아도 돼요.
POST /collections/{collection_name}/points/scroll
{
"filter": {
"must": [{
"nested": {
"key": "diet",
"filter":{
"must": [
{
"key": "food",
"match": {
"value": "meat"
}
},
{
"key": "likes",
"match": {
"value": true
}
}
]
}
}
}]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must=[
models.NestedCondition(
nested=models.Nested(
key="diet",
filter=models.Filter(
must=[
models.FieldCondition(
key="food", match=models.MatchValue(value="meat")
),
models.FieldCondition(
key="likes", match=models.MatchValue(value=True)
),
]
),
)
)
],
),
)
client.scroll("{collection_name}", {
filter: {
must: [
{
nested: {
key: "diet",
filter: {
must: [
{
key: "food",
match: { value: "meat" },
},
{
key: "likes",
match: { value: true },
},
],
},
},
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, NestedCondition, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must([NestedCondition {
key: "diet".to_string(),
filter: Some(Filter::must([
Condition::matches("food", "meat".to_string()),
Condition::matches("likes", true),
])),
}
.into()])),
)
.await?;
import static io.qdrant.client.ConditionFactory.match;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.ConditionFactory.nested;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addMust(
nested(
"diet",
Filter.newBuilder()
.addAllMust(
List.of(
matchKeyword("food", "meat"), match("likes", true)))
.build()))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: Nested("diet", MatchKeyword("food", "meat") & Match("likes", true))
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewNestedFilter("diet", &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("food", "meat"),
qdrant.NewMatchBool("likes", true),
},
}),
},
},
})
중첩 필터를 쓰면 매칭 로직이 payload 안의 배열 요소(원소) 하나 단위에서 적용돼요. 마치 중첩 필터를 배열 요소 하나에 하나씩 적용하는 것과 같아요. 그리고 부모 문서(포인트)는 배열의 요소 중 하나라도 중첩 필터를 만족하면 매칭된 걸로 봐요.
제약 (Limitations)
has_id와 slice 조건은 중첩 객체 필터 안에서는 지원되지 않아요. 이 조건이 필요하면, 옆에 있는(adjacent) must 절에 따로 넣어주면 돼요.
POST /collections/{collection_name}/points/scroll
{
"filter":{
"must":[
{
"nested":{
"key":"diet",
"filter":{
"must":[
{
"key":"food",
"match":{
"value":"meat"
}
},
{
"key":"likes",
"match":{
"value":true
}
}
]
}
}
},
{
"has_id":[
1
]
}
]
}
}
client.scroll(
collection_name="{collection_name}",
scroll_filter=models.Filter(
must=[
models.NestedCondition(
nested=models.Nested(
key="diet",
filter=models.Filter(
must=[
models.FieldCondition(
key="food", match=models.MatchValue(value="meat")
),
models.FieldCondition(
key="likes", match=models.MatchValue(value=True)
),
]
),
)
),
models.HasIdCondition(has_id=[1]),
],
),
)
client.scroll("{collection_name}", {
filter: {
must: [
{
nested: {
key: "diet",
filter: {
must: [
{
key: "food",
match: { value: "meat" },
},
{
key: "likes",
match: { value: true },
},
],
},
},
},
{
has_id: [1],
},
],
},
});
use qdrant_client::qdrant::{Condition, Filter, NestedCondition, ScrollPointsBuilder};
client
.scroll(
ScrollPointsBuilder::new("{collection_name}").filter(Filter::must([
NestedCondition {
key: "diet".to_string(),
filter: Some(Filter::must([
Condition::matches("food", "meat".to_string()),
Condition::matches("likes", true),
])),
}
.into(),
Condition::has_id([1]),
])),
)
.await?;
import static io.qdrant.client.ConditionFactory.hasId;
import static io.qdrant.client.ConditionFactory.match;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.ConditionFactory.nested;
import static io.qdrant.client.PointIdFactory.id;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.ScrollPoints;
import java.util.List;
client
.scrollAsync(
ScrollPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder()
.addMust(
nested(
"diet",
Filter.newBuilder()
.addAllMust(
List.of(
matchKeyword("food", "meat"), match("likes", true)))
.build()))
.addMust(hasId(id(1)))
.build())
.build())
.get();
using Qdrant.Client;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.ScrollAsync(
collectionName: "{collection_name}",
filter: Nested("diet", MatchKeyword("food", "meat") & Match("likes", true)) & HasId(1)
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Scroll(context.Background(), &qdrant.ScrollPoints{
CollectionName: "{collection_name}",
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewNestedFilter("diet", &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("food", "meat"),
qdrant.NewMatchBool("likes", true),
},
}),
qdrant.NewHasID(qdrant.NewIDNum(1)),
},
},
})
Prefix Match
Available as of v1.19.0
prefix 매치는 지정한 문자열로 시작하는 keyword 값을 매칭해요. 예를 들어 prefix "https://qdrant."는 "https://qdrant.tech/documentation"와 매칭되지만, prefix "qdrant"는 그렇지 않아요.
매칭은 바이트 단위로 이뤄지기 때문에, 유효한 UTF-8 문자열에서는 곧 문자 단위 매칭과 같아요. 또 keyword 정확 매칭과 일관되게 대소문자를 구분해요. Full Text Match와 달리 value를 토큰화하지 않아서, URL·경로·SKU 같은 식별자(identifier)에 특히 잘 맞아요.
{
"key": "url",
"match": {
"prefix": "https://qdrant."
}
}
models.FieldCondition(
key="url",
match=models.MatchPrefix(prefix="https://qdrant."),
)
{
key: 'url',
match: {prefix: 'https://qdrant.'}
}
use qdrant_client::qdrant::Condition;
Condition::matches_prefix("url", "https://qdrant.")
import static io.qdrant.client.ConditionFactory.matchPrefix;
matchPrefix("url", "https://qdrant.");
using static Qdrant.Client.Grpc.Conditions;
MatchPrefix("url", "https://qdrant.");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchPrefix("url", "https://qdrant.")
Full Text Match
Available as of v0.10.0
match 조건의 특수한 경우로, text match 조건이 있어요. 텍스트 필드 안에서 특정 부분 문자열(substring), 토큰, 구(phrase)를 검색할 수 있게 해줘요.
정확히 어떤 텍스트가 매칭되는지는 full-text index 설정에 달려 있어요. 이 설정은 인덱스를 만들 때 정의하며, full-text index에서 설명해요. 만약 해당 필드에 full-text index가 없다면, 기본적인 토크나이저(tokenizer)를 사용해요.
{
"key": "description",
"match": {
"text": "good cheap"
}
}
models.FieldCondition(
key="description",
match=models.MatchText(text="good cheap"),
)
{
key: 'description',
match: {text: 'good cheap'}
}
use qdrant_client::qdrant::Condition;
Condition::matches_text("description", "good cheap")
import static io.qdrant.client.ConditionFactory.matchText;
matchText("description", "good cheap");
using static Qdrant.Client.Grpc.Conditions;
MatchText("description", "good cheap");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchText("description", "good cheap")
쿼리에 단어가 여러 개 있으면, 텍스트 안에 그 단어들이 모두 있어야 조건이 만족돼요.
Full Text Any
Available as of v1.16.0
text_any full-text match 조건은 text 조건과 비슷하지만 핵심 차이가 있어요. text는 쿼리 용어를 모두 포함한 텍스트 필드만 매칭하는 반면, text_any는 쿼리 용어 중 하나라도 포함한 필드를 매칭해요. 다시 말해 텍스트 필드에 쿼리 용어가 단 하나만 있어도 매치된 걸로 봐요.
예를 들어 good cheap이라는 쿼리는 cheap hardware와도, good performance와도 매칭돼요.
{
"key": "description",
"match": {
"text_any": "good cheap"
}
}
models.FieldCondition(
key="description",
match=models.MatchTextAny(text_any="good cheap"),
)
{
key: 'description',
match: {text_any: 'good cheap'}
}
use qdrant_client::qdrant::Condition;
Condition::matches_text_any("description", "good cheap")
import static io.qdrant.client.ConditionFactory.matchTextAny;
matchTextAny("description", "good cheap");
using static Qdrant.Client.Grpc.Conditions;
MatchTextAny("description", "good cheap");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchTextAny("description", "good cheap")
Phrase Match
Available as of v1.15.0
phrase match 조건도 full-text index를 활용해서 정확한 구(phrase) 비교를 수행해요. 텍스트 필드 안에서 특정 토큰 구를 검색할 수 있게 해줘요.
예를 들어 텍스트 "quick brown fox"는 쿼리 "brown fox"와는 매칭되지만, "fox brown"과는 매칭되지 않아요. 순서까지 고려한 정확한 구 매칭이거든요.
만약 해당 필드에 full-text index가 없다면 기본적인 토크나이저를 사용해요.
{
"key": "description",
"match": {
"phrase": "brown fox"
}
}
models.FieldCondition(
key="description",
match=models.MatchPhrase(phrase="brown fox"),
)
{
key: 'description',
match: {phrase: 'brown fox'}
}
use qdrant_client::qdrant::Condition;
Condition::matches_phrase("description", "brown fox")
import static io.qdrant.client.ConditionFactory.matchPhrase;
matchPhrase("description", "brown fox");
using static Qdrant.Client.Grpc.Conditions;
MatchPhrase("description", "brown fox");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewMatchPhrase("description", "brown fox")
Range
range 조건은 저장된 payload 값에 대해 가능한 값의 범위를 정해요. 여러 값이 저장돼 있다면 그중 하나라도 조건을 만족하면 돼요.
{
"key": "price",
"range": {
"gt": null,
"gte": 100.0,
"lt": null,
"lte": 450.0
}
}
models.FieldCondition(
key="price",
range=models.Range(
gt=None,
gte=100.0,
lt=None,
lte=450.0,
),
)
{
key: 'price',
range: {
gt: null,
gte: 100.0,
lt: null,
lte: 450.0
}
}
use qdrant_client::qdrant::{Condition, Range};
Condition::range(
"price",
Range {
gt: None,
gte: Some(100.0),
lt: None,
lte: Some(450.0),
},
)
import static io.qdrant.client.ConditionFactory.range;
import io.qdrant.client.grpc.Common.Range;
range("price", Range.newBuilder().setGte(100.0).setLte(450).build());
using static Qdrant.Client.Grpc.Conditions;
Range("price", new Qdrant.Client.Grpc.Range { Gte = 100.0, Lte = 450 });
import "github.com/qdrant/go-client/qdrant"
qdrant.NewRange("price", &qdrant.Range{
Gte: qdrant.PtrOf(100.0),
Lte: qdrant.PtrOf(450.0),
})
사용할 수 있는 비교 연산은 이렇게 네 가지예요.
gt– greater than (보다 큼)gte– greater than or equal (보다 크거나 같음)lt– less than (보다 작음)lte– less than or equal (보다 작거나 같음)
Datetime Range
Available as of v1.8.0
datetime range는 특별한 종류의 range 조건으로, datetime payload에 사용해요. RFC 3339 형식을 지원해요. 날짜를 UNIX 타임스탬프로 변환할 필요가 없어요. 비교할 때 타임스탬프는 파싱되어 UTC로 변환돼요.
{
"key": "created_at",
"range": {
"gte": "2023-01-01T00:00:00Z",
"lte": "2023-12-31T23:59:59Z"
}
}
models.FieldCondition(
key="created_at",
range=models.DatetimeRange(
gte="2023-01-01T00:00:00Z",
lte="2023-12-31T23:59:59Z",
),
)
{
key: 'created_at',
range: {
gte: '2023-01-01T00:00:00Z',
lte: '2023-12-31T23:59:59Z'
}
}
use qdrant_client::qdrant::Condition;
Condition::datetime_range(
"created_at",
"2023-01-01T00:00:00Z".into(),
"2023-12-31T23:59:59Z".into(),
)
import static io.qdrant.client.ConditionFactory.datetimeRange;
datetimeRange("created_at", "2023-01-01T00:00:00Z", "2023-12-31T23:59:59Z");
using static Qdrant.Client.Grpc.Conditions;
DatetimeRange("created_at", "2023-01-01T00:00:00Z", "2023-12-31T23:59:59Z");
import "github.com/qdrant/go-client/qdrant"
qdrant.NewDatetimeRange("created_at", "2023-01-01T00:00:00Z", "2023-12-31T23:59:59Z")
이렇게 Qdrant의 필터링을 절(clause)과 조건(condition) 두 축으로 살펴봤어요. 절로 AND/OR/NOT 같은 논리를 조합하고, 조건으로 payload 값의 타입에 맞는 세밀한 비교를 해요. 특히 중첩 객체 필터는 배열 요소 단위로 매칭이 필요한 복잡한 데이터를 다룰 때 꼭 필요하니, 기억해두면 좋아요.