milvus-logo
LFAI
Home
  • Guia do utilizador

Pesquisa num único vetor

Uma vez inseridos os dados, a etapa seguinte consiste em efetuar pesquisas de semelhança da sua coleção no Milvus.

Milvus permite-lhe efetuar dois tipos de pesquisas, em função do número de campos vectoriais da sua coleção:

  • Pesquisa de um único vetor: Se a sua coleção tiver apenas um campo vetorial, utilize o método search() para encontrar as entidades mais semelhantes. Este método compara o seu vetor de consulta com os vectores existentes na sua coleção e devolve as IDs das correspondências mais próximas juntamente com as distâncias entre elas. Opcionalmente, também pode devolver os valores do vetor e os metadados dos resultados.
  • Pesquisa híbrida: Para colecções com dois ou mais campos vectoriais, utilize o método hybrid_search() método. Esse método executa várias solicitações de pesquisa ANN (Approximate Nearest Neighbor) e combina os resultados para retornar as correspondências mais relevantes após a reavaliação.

Este guia centra-se na forma de efetuar uma pesquisa de vetor único em Milvus. Para obter detalhes sobre a pesquisa híbrida, consulte Pesquisa híbrida.

Visão geral

Há uma variedade de tipos de pesquisa para atender a diferentes requisitos:

  • Pesquisa básica: Inclui pesquisa de vetor único, pesquisa de vetor em massa, pesquisa de partição e pesquisa com campos de saída especificados.

  • Pesquisa filtrada: Aplica critérios de filtragem baseados em campos escalares para refinar os resultados da pesquisa.

  • Pesquisa de intervalo: Encontra vetores dentro de um intervalo de distância específico do vetor de consulta.

  • Pesquisa de agrupamento: Agrupa os resultados da pesquisa com base num campo específico para garantir a diversidade dos resultados.

Preparações

O trecho de código abaixo reaproveita o código existente para estabelecer uma conexão com o Milvus e configurar rapidamente uma coleção.

# 1. Set up a Milvus client
client = MilvusClient(
    uri=CLUSTER_ENDPOINT,
    token=TOKEN 
)

# 2. Create a collection
client.create_collection(
    collection_name="quick_setup",
    dimension=5,
    metric_type="IP"
)

# 3. Insert randomly generated vectors 
colors = ["green", "blue", "yellow", "red", "black", "white", "purple", "pink", "orange", "brown", "grey"]
data = []

for i in range(1000):
    current_color = random.choice(colors)
    data.append({
        "id": i,
        "vector": [ random.uniform(-1, 1) for _ in range(5) ],
        "color": current_color,
        "color_tag": f"{current_color}_{str(random.randint(1000, 9999))}"
    })

res = client.insert(
    collection_name="quick_setup",
    data=data
)

print(res)

# Output
#
# {
#     "insert_count": 1000,
#     "ids": [
#         0,
#         1,
#         2,
#         3,
#         4,
#         5,
#         6,
#         7,
#         8,
#         9,
#         "(990 more items hidden)"
#     ]
# }

# 6.1 Create partitions 
client.create_partition(
    collection_name="quick_setup",
    partition_name="red"
)

client.create_partition(
    collection_name="quick_setup",
    partition_name="blue"
)

# 6.1 Insert data into partitions
red_data = [ {"id": i, "vector": [ random.uniform(-1, 1) for _ in range(5) ], "color": "red", "color_tag": f"red_{str(random.randint(1000, 9999))}" } for i in range(500) ]
blue_data = [ {"id": i, "vector": [ random.uniform(-1, 1) for _ in range(5) ], "color": "blue", "color_tag": f"blue_{str(random.randint(1000, 9999))}" } for i in range(500) ]

res = client.insert(
    collection_name="quick_setup",
    data=red_data,
    partition_name="red"
)

print(res)

# Output
#
# {
#     "insert_count": 500,
#     "ids": [
#         0,
#         1,
#         2,
#         3,
#         4,
#         5,
#         6,
#         7,
#         8,
#         9,
#         "(490 more items hidden)"
#     ]
# }

res = client.insert(
    collection_name="quick_setup",
    data=blue_data,
    partition_name="blue"
)

print(res)

# Output
#
# {
#     "insert_count": 500,
#     "ids": [
#         0,
#         1,
#         2,
#         3,
#         4,
#         5,
#         6,
#         7,
#         8,
#         9,
#         "(490 more items hidden)"
#     ]
# }
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;

import com.alibaba.fastjson.JSONObject;

import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
import io.milvus.v2.service.collection.request.GetLoadStateReq;
import io.milvus.v2.service.vector.request.InsertReq;
import io.milvus.v2.service.vector.response.InsertResp; 

String CLUSTER_ENDPOINT = "http://localhost:19530";

// 1. Connect to Milvus server
ConnectConfig connectConfig = ConnectConfig.builder()
    .uri(CLUSTER_ENDPOINT)
    .build();

MilvusClientV2 client = new MilvusClientV2(connectConfig);  

// 2. Create a collection in quick setup mode
CreateCollectionReq quickSetupReq = CreateCollectionReq.builder()
    .collectionName("quick_setup")
    .dimension(5)
    .metricType("IP")
    .build();

client.createCollection(quickSetupReq);

GetLoadStateReq loadStateReq = GetLoadStateReq.builder()
    .collectionName("quick_setup")
    .build();

boolean state = client.getLoadState(loadStateReq);

System.out.println(state);

// Output:
// true

// 3. Insert randomly generated vectors into the collection
List<String> colors = Arrays.asList("green", "blue", "yellow", "red", "black", "white", "purple", "pink", "orange", "brown", "grey");
List<JSONObject> data = new ArrayList<>();

for (int i=0; i<1000; i++) {
    Random rand = new Random();
    String current_color = colors.get(rand.nextInt(colors.size()-1));
    JSONObject row = new JSONObject();
    row.put("id", Long.valueOf(i));
    row.put("vector", Arrays.asList(rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat()));
    row.put("color_tag", current_color + "_" + String.valueOf(rand.nextInt(8999) + 1000));
    data.add(row);
}

InsertReq insertReq = InsertReq.builder()
    .collectionName("quick_setup")
    .data(data)
    .build();

InsertResp insertResp = client.insert(insertReq);

System.out.println(JSONObject.toJSON(insertResp));

// Output:
// {"insertCnt": 1000}

// 6.1. Create a partition
CreatePartitionReq partitionReq = CreatePartitionReq.builder()
    .collectionName("quick_setup")
    .partitionName("red")
    .build();

client.createPartition(partitionReq);

partitionReq = CreatePartitionReq.builder()
    .collectionName("quick_setup")
    .partitionName("blue")
    .build();

client.createPartition(partitionReq);

// 6.2 Insert data into the partition
data = new ArrayList<>();

for (int i=1000; i<1500; i++) {
    Random rand = new Random();
    String current_color = "red";
    JSONObject row = new JSONObject();
    row.put("id", Long.valueOf(i));
    row.put("vector", Arrays.asList(rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat()));
    row.put("color", current_color);
    row.put("color_tag", current_color + "_" + String.valueOf(rand.nextInt(8999) + 1000));
    data.add(row);
}     

insertReq = InsertReq.builder()
    .collectionName("quick_setup")
    .data(data)
    .partitionName("red")
    .build();

insertResp = client.insert(insertReq);

System.out.println(JSONObject.toJSON(insertResp));

// Output:
// {"insertCnt": 500}

data = new ArrayList<>();

for (int i=1500; i<2000; i++) {
    Random rand = new Random();
    String current_color = "blue";
    JSONObject row = new JSONObject();
    row.put("id", Long.valueOf(i));
    row.put("vector", Arrays.asList(rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), rand.nextFloat()));
    row.put("color", current_color);
    row.put("color_tag", current_color + "_" + String.valueOf(rand.nextInt(8999) + 1000));
    data.add(row);
}

insertReq = InsertReq.builder()
    .collectionName("quick_setup")
    .data(data)
    .partitionName("blue")
    .build();

insertResp = client.insert(insertReq);

System.out.println(JSONObject.toJSON(insertResp));

// Output:
// {"insertCnt": 500}
const { MilvusClient, DataType, sleep } = require("@zilliz/milvus2-sdk-node")

const address = "http://localhost:19530"

// 1. Set up a Milvus Client
client = new MilvusClient({address});

// 2. Create a collection in quick setup mode
await client.createCollection({
    collection_name: "quick_setup",
    dimension: 5,
    metric_type: "IP"
});  

// 3. Insert randomly generated vectors
const colors = ["green", "blue", "yellow", "red", "black", "white", "purple", "pink", "orange", "brown", "grey"]
data = []

for (let i = 0; i < 1000; i++) {
    current_color = colors[Math.floor(Math.random() * colors.length)]
    data.push({
        id: i,
        vector: [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()],
        color: current_color,
        color_tag: `${current_color}_${Math.floor(Math.random() * 8999) + 1000}`
    })
}

var res = await client.insert({
    collection_name: "quick_setup",
    data: data
})

console.log(res.insert_cnt)

// Output
// 
// 1000
// 

await client.createPartition({
    collection_name: "quick_setup",
    partition_name: "red"
})

await client.createPartition({
    collection_name: "quick_setup",
    partition_name: "blue"
})

// 6.1 Insert data into partitions
var red_data = []
var blue_data = []

for (let i = 1000; i < 1500; i++) {
    red_data.push({
        id: i,
        vector: [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()],
        color: "red",
        color_tag: `red_${Math.floor(Math.random() * 8999) + 1000}`
    })
}

for (let i = 1500; i < 2000; i++) {
    blue_data.push({
        id: i,
        vector: [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()],
        color: "blue",
        color_tag: `blue_${Math.floor(Math.random() * 8999) + 1000}`
    })
}

res = await client.insert({
    collection_name: "quick_setup",
    data: red_data,
    partition_name: "red"
})

console.log(res.insert_cnt)

// Output
// 
// 500
// 

res = await client.insert({
    collection_name: "quick_setup",
    data: blue_data,
    partition_name: "blue"
})

console.log(res.insert_cnt)

// Output
// 
// 500
// 

Ao enviar um pedido search, pode fornecer um ou mais valores vectoriais que representam os embeddings da sua consulta e um valor limit que indica o número de resultados a devolver.

Dependendo dos seus dados e do seu vetor de consulta, pode obter menos do que limit resultados. Isto acontece quando limit é maior do que o número de possíveis vectores de correspondência para a sua consulta.

A pesquisa de vetor único é a forma mais simples de search operações em Milvus, concebida para encontrar os vectores mais semelhantes a um determinado vetor de consulta.

Para efetuar uma pesquisa de vetor único, especifique o nome da coleção de destino, o vetor de consulta e o número de resultados pretendido (limit). Esta operação devolve um conjunto de resultados que inclui os vectores mais semelhantes, os respectivos IDs e as distâncias do vetor de consulta.

Aqui está um exemplo de pesquisa das 5 principais entidades que são mais semelhantes ao vetor de consulta:

# Single vector search
res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    # Replace with your query vector
    data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
    limit=5, # Max. number of search results to return
    search_params={"metric_type": "IP", "params": {}} # Search parameters
)

# Convert the output to a formatted JSON string
result = json.dumps(res, indent=4)
print(result)
// 4. Single vector search
List<List<Float>> query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

SearchReq searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .topK(3) // The number of results to return
    .build();

SearchResp searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 4. Single vector search
var query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592],

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    limit: 3, // The number of results to return
})

console.log(res.results)
Parâmetro Descrição
collection_name O nome de uma coleção existente.
data Uma lista de incorporações vectoriais.
Milvus procura as incorporações vectoriais mais semelhantes às especificadas.
limit O número total de entidades a devolver.
Pode utilizar este parâmetro em combinação com offset em param para ativar a paginação.
A soma deste valor e de offset em param deve ser inferior a 16.384.
search_params As definições de parâmetros específicas para esta operação.
  • metric_type: O tipo de métrica aplicado a esta operação. Este deve ser o mesmo que o utilizado quando indexa o campo vetorial especificado acima. Os valores possíveis são L2, IP, COSINE, JACCARD, HAMMING.
  • params: Parâmetros adicionais. Para mais pormenores, consulte search().
Parâmetro Descrição
collectionName O nome de uma coleção existente.
data Uma lista de incorporações vectoriais.
Milvus procura as incorporações vectoriais mais semelhantes às especificadas.
topK O número de registos a devolver no resultado da pesquisa. Este parâmetro utiliza a mesma sintaxe que o parâmetro limit, pelo que só deve definir um deles.
Pode utilizar este parâmetro em combinação com offset em param para ativar a paginação.
A soma deste valor e de offset em param deve ser inferior a 16.384.
Parâmetro Descrição
collection_name O nome de uma coleção existente.
data Uma lista de entidades vectoriais.
O Milvus procura as entidades vectoriais mais semelhantes às especificadas.
limit O número total de entidades a retornar.
Você pode usar este parâmetro em combinação com offset em param para habilitar a paginação.
A soma deste valor e offset em param deve ser menor que 16.384.

A saída é semelhante à seguinte:

[
    [
        {
            "id": 0,
            "distance": 1.4093276262283325,
            "entity": {}
        },
        {
            "id": 4,
            "distance": 0.9902134537696838,
            "entity": {}
        },
        {
            "id": 1,
            "distance": 0.8519943356513977,
            "entity": {}
        },
        {
            "id": 5,
            "distance": 0.7972343564033508,
            "entity": {}
        },
        {
            "id": 2,
            "distance": 0.5928734540939331,
            "entity": {}
        }
    ]
]
{"searchResults": [[
    {
        "score": 1.263043,
        "fields": {
            "vector": [
                0.9533119,
                0.02538395,
                0.76714665,
                0.35481733,
                0.9845762
            ],
            "id": 740
        }
    },
    {
        "score": 1.2377806,
        "fields": {
            "vector": [
                0.7411156,
                0.08687937,
                0.8254139,
                0.08370924,
                0.99095553
            ],
            "id": 640
        }
    },
    {
        "score": 1.1869997,
        "fields": {
            "vector": [
                0.87928146,
                0.05324632,
                0.6312755,
                0.28005534,
                0.9542448
            ],
            "id": 455
        }
    }
]]}
[
  { score: 1.7463608980178833, id: '854' },
  { score: 1.744946002960205, id: '425' },
  { score: 1.7258622646331787, id: '718' }
]

O resultado mostra os 5 vizinhos mais próximos do seu vetor de consulta, incluindo os seus IDs únicos e as distâncias calculadas.

Uma pesquisa de vetor em massa estende o conceito de pesquisa de vetor único, permitindo que vários vetores de consulta sejam pesquisados em uma única solicitação. Este tipo de pesquisa é ideal para cenários em que é necessário encontrar vectores semelhantes para um conjunto de vectores de consulta, reduzindo significativamente o tempo e os recursos computacionais necessários.

Numa pesquisa de vectores em massa, é possível incluir vários vectores de consulta no campo data. O sistema processa estes vectores em paralelo, devolvendo um conjunto de resultados separado para cada vetor de consulta, cada conjunto contendo as correspondências mais próximas encontradas na coleção.

Aqui está um exemplo de pesquisa de dois conjuntos distintos das entidades mais semelhantes de dois vectores de consulta:

# Bulk-vector search
res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    data=[
        [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104],
        [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995, 0.95791889146345]
    ], # Replace with your query vectors
    limit=2, # Max. number of search results to return
    search_params={"metric_type": "IP", "params": {}} # Search parameters
)

result = json.dumps(res, indent=4)
print(result)
// 5. Batch vector search
query_vectors = Arrays.asList(
    Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f),
    Arrays.asList(0.19886812562848388f, 0.06023560599112088f, 0.6976963061752597f, 0.2614474506242501f, 0.838729485096104f)
);

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .topK(2)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 5. Batch vector search
var query_vectors = [
    [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592],
    [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104]
]

res = await client.search({
    collection_name: "quick_setup",
    data: query_vectors,
    limit: 2,
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 1,
            "distance": 1.3017789125442505,
            "entity": {}
        },
        {
            "id": 7,
            "distance": 1.2419954538345337,
            "entity": {}
        }
    ], # Result set 1
    [
        {
            "id": 3,
            "distance": 2.3358664512634277,
            "entity": {}
        },
        {
            "id": 8,
            "distance": 0.5642921924591064,
            "entity": {}
        }
    ] # Result set 2
]
// Two sets of vectors are returned as expected

{"searchResults": [
    [
        {
            "score": 1.263043,
            "fields": {
                "vector": [
                    0.9533119,
                    0.02538395,
                    0.76714665,
                    0.35481733,
                    0.9845762
                ],
                "id": 740
            }
        },
        {
            "score": 1.2377806,
            "fields": {
                "vector": [
                    0.7411156,
                    0.08687937,
                    0.8254139,
                    0.08370924,
                    0.99095553
                ],
                "id": 640
            }
        }
    ],
    [
        {
            "score": 1.8654699,
            "fields": {
                "vector": [
                    0.4671427,
                    0.8378432,
                    0.98844475,
                    0.82763994,
                    0.9729997
                ],
                "id": 638
            }
        },
        {
            "score": 1.8581753,
            "fields": {
                "vector": [
                    0.735541,
                    0.60140246,
                    0.86730254,
                    0.93152493,
                    0.98603314
                ],
                "id": 855
            }
        }
    ]
]}
[
  [
    { score: 2.3590476512908936, id: '854' },
    { score: 2.2896690368652344, id: '59' }
  [
    { score: 2.664059638977051, id: '59' },
    { score: 2.59483003616333, id: '854' }
  ]
]

Os resultados incluem dois conjuntos de vizinhos mais próximos, um para cada vetor de consulta, demonstrando a eficiência das pesquisas de vectores em massa no tratamento de vários vectores de consulta de uma só vez.

A pesquisa por partição limita o âmbito da sua pesquisa a um subconjunto ou partição específica da sua coleção. Isto é particularmente útil para conjuntos de dados organizados em que os dados são segmentados em divisões lógicas ou categóricas, permitindo operações de pesquisa mais rápidas ao reduzir o volume de dados a pesquisar.

Para efetuar uma pesquisa de partição, basta incluir o nome da partição alvo em partition_names do seu pedido de pesquisa. Isto especifica que a operação search apenas considera os vectores dentro da partição especificada.

Aqui está um exemplo de pesquisa de entidades em red:

# 6.2 Search within a partition
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = client.search(
    collection_name="quick_setup",
    data=[query_vector],
    limit=5,
    search_params={"metric_type": "IP", "params": {"level": 1}},
    partition_names=["red"]
)

print(res)
// 6.3 Search within partitions
query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .partitionNames(Arrays.asList("red"))
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 6.2 Search within partitions
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    partition_names: ["red"],
    limit: 5,
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 16,
            "distance": 0.9200337529182434,
            "entity": {}
        },
        {
            "id": 14,
            "distance": 0.4505271911621094,
            "entity": {}
        },
        {
            "id": 15,
            "distance": 0.19924677908420563,
            "entity": {}
        },
        {
            "id": 17,
            "distance": 0.0075093843042850494,
            "entity": {}
        },
        {
            "id": 13,
            "distance": -0.14609718322753906,
            "entity": {}
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.1677284,
            "fields": {
                "vector": [
                    0.9986977,
                    0.17964739,
                    0.49086612,
                    0.23155272,
                    0.98438674
                ],
                "id": 1435
            }
        },
        {
            "score": 1.1476475,
            "fields": {
                "vector": [
                    0.6952647,
                    0.13417172,
                    0.91045254,
                    0.119336545,
                    0.9338931
                ],
                "id": 1291
            }
        },
        {
            "score": 1.0969629,
            "fields": {
                "vector": [
                    0.3363194,
                    0.028906643,
                    0.6675426,
                    0.030419827,
                    0.9735209
                ],
                "id": 1168
            }
        },
        {
            "score": 1.0741848,
            "fields": {
                "vector": [
                    0.9980543,
                    0.36063594,
                    0.66427994,
                    0.17359233,
                    0.94954175
                ],
                "id": 1164
            }
        },
        {
            "score": 1.0584627,
            "fields": {
                "vector": [
                    0.7187005,
                    0.12674773,
                    0.987718,
                    0.3110777,
                    0.86093885
                ],
                "id": 1085
            }
        }
    ],
    [
        {
            "score": 1.8030131,
            "fields": {
                "vector": [
                    0.59726167,
                    0.7054632,
                    0.9573117,
                    0.94529945,
                    0.8664103
                ],
                "id": 1203
            }
        },
        {
            "score": 1.7728865,
            "fields": {
                "vector": [
                    0.6672442,
                    0.60448086,
                    0.9325822,
                    0.80272985,
                    0.8861626
                ],
                "id": 1448
            }
        },
        {
            "score": 1.7536311,
            "fields": {
                "vector": [
                    0.59663296,
                    0.77831805,
                    0.8578314,
                    0.88818026,
                    0.9030075
                ],
                "id": 1010
            }
        },
        {
            "score": 1.7520742,
            "fields": {
                "vector": [
                    0.854198,
                    0.72294194,
                    0.9245805,
                    0.86126596,
                    0.7969224
                ],
                "id": 1219
            }
        },
        {
            "score": 1.7452049,
            "fields": {
                "vector": [
                    0.96419,
                    0.943535,
                    0.87611496,
                    0.8268136,
                    0.79786557
                ],
                "id": 1149
            }
        }
    ]
]}
[
  { score: 3.0258803367614746, id: '1201' },
  { score: 3.004319190979004, id: '1458' },
  { score: 2.880324363708496, id: '1187' },
  { score: 2.8246407508850098, id: '1347' },
  { score: 2.797295093536377, id: '1406' }
]

Em seguida, procure entidades em blue:

res = client.search(
    collection_name="quick_setup",
    data=[query_vector],
    limit=5,
    search_params={"metric_type": "IP", "params": {"level": 1}},
    partition_names=["blue"]
)

print(res)
searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .partitionNames(Arrays.asList("blue"))
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    partition_names: ["blue"],
    limit: 5,
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 20,
            "distance": 2.363696813583374,
            "entity": {}
        },
        {
            "id": 26,
            "distance": 1.0665391683578491,
            "entity": {}
        },
        {
            "id": 23,
            "distance": 1.066049575805664,
            "entity": {}
        },
        {
            "id": 29,
            "distance": 0.8353596925735474,
            "entity": {}
        },
        {
            "id": 28,
            "distance": 0.7484277486801147,
            "entity": {}
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.1628494,
            "fields": {
                "vector": [
                    0.7442872,
                    0.046407282,
                    0.71031404,
                    0.3544345,
                    0.9819991
                ],
                "id": 1992
            }
        },
        {
            "score": 1.1470042,
            "fields": {
                "vector": [
                    0.5505825,
                    0.04367262,
                    0.9985836,
                    0.18922359,
                    0.93255126
                ],
                "id": 1977
            }
        },
        {
            "score": 1.1450152,
            "fields": {
                "vector": [
                    0.89994013,
                    0.052991092,
                    0.8645576,
                    0.6406729,
                    0.95679337
                ],
                "id": 1573
            }
        },
        {
            "score": 1.1439825,
            "fields": {
                "vector": [
                    0.9253267,
                    0.15890503,
                    0.7999555,
                    0.19126713,
                    0.898583
                ],
                "id": 1552
            }
        },
        {
            "score": 1.1029172,
            "fields": {
                "vector": [
                    0.95661926,
                    0.18777144,
                    0.38115507,
                    0.14323527,
                    0.93137646
                ],
                "id": 1823
            }
        }
    ],
    [
        {
            "score": 1.8005109,
            "fields": {
                "vector": [
                    0.5953582,
                    0.7794224,
                    0.9388869,
                    0.79825854,
                    0.9197286
                ],
                "id": 1888
            }
        },
        {
            "score": 1.7714822,
            "fields": {
                "vector": [
                    0.56805456,
                    0.89422905,
                    0.88187534,
                    0.914824,
                    0.8944365
                ],
                "id": 1648
            }
        },
        {
            "score": 1.7561421,
            "fields": {
                "vector": [
                    0.83421993,
                    0.39865613,
                    0.92319834,
                    0.42695504,
                    0.96633124
                ],
                "id": 1688
            }
        },
        {
            "score": 1.7553532,
            "fields": {
                "vector": [
                    0.89994013,
                    0.052991092,
                    0.8645576,
                    0.6406729,
                    0.95679337
                ],
                "id": 1573
            }
        },
        {
            "score": 1.7543385,
            "fields": {
                "vector": [
                    0.16542226,
                    0.38248396,
                    0.9888778,
                    0.80913955,
                    0.9501492
                ],
                "id": 1544
            }
        }
    ]
]}
[
  { score: 2.8421106338500977, id: '1745' },
  { score: 2.838560104370117, id: '1782' },
  { score: 2.8134000301361084, id: '1511' },
  { score: 2.718268871307373, id: '1679' },
  { score: 2.7014894485473633, id: '1597' }
]

Os dados em red são diferentes dos dados em blue. Por conseguinte, os resultados da pesquisa serão limitados à partição especificada, reflectindo as caraterísticas únicas e a distribuição de dados desse subconjunto.

Pesquisa com campos de saída

A pesquisa com campos de saída permite-lhe especificar quais os atributos ou campos dos vectores correspondentes que devem ser incluídos nos resultados da pesquisa.

É possível especificar output_fields num pedido para devolver resultados com campos específicos.

Aqui está um exemplo de retorno de resultados com valores de atributo color:

# Search with output fields
res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
    limit=5, # Max. number of search results to return
    search_params={"metric_type": "IP", "params": {}}, # Search parameters
    output_fields=["color"] # Output fields to return
)

result = json.dumps(res, indent=4)
print(result)
// 7. Search with output fields
query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .outputFields(Arrays.asList("color"))
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 7. Search with output fields
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    limit: 5,
    output_fields: ["color"],
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 0,
            "distance": 1.4093276262283325,
            "entity": {
                "color": "pink_8682"
            }
        },
        {
            "id": 16,
            "distance": 1.0159327983856201,
            "entity": {
                "color": "yellow_1496"
            }
        },
        {
            "id": 4,
            "distance": 0.9902134537696838,
            "entity": {
                "color": "red_4794"
            }
        },
        {
            "id": 14,
            "distance": 0.9803846478462219,
            "entity": {
                "color": "green_2899"
            }
        },
        {
            "id": 1,
            "distance": 0.8519943356513977,
            "entity": {
                "color": "red_7025"
            }
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.263043,
            "fields": {}
        },
        {
            "score": 1.2377806,
            "fields": {}
        },
        {
            "score": 1.1869997,
            "fields": {}
        },
        {
            "score": 1.1748955,
            "fields": {}
        },
        {
            "score": 1.1720343,
            "fields": {}
        }
    ]
]}

[
  { score: 3.036271572113037, id: '59', color: 'orange' },
  { score: 3.0267879962921143, id: '1745', color: 'blue' },
  { score: 3.0069446563720703, id: '854', color: 'black' },
  { score: 2.984386682510376, id: '718', color: 'black' },
  { score: 2.916019916534424, id: '425', color: 'purple' }
]

Juntamente com os vizinhos mais próximos, os resultados da pesquisa incluirão o campo especificado color, fornecendo um conjunto mais rico de informações para cada vetor correspondente.

A pesquisa filtrada aplica filtros escalares a pesquisas vectoriais, permitindo-lhe refinar os resultados da pesquisa com base em critérios específicos. Pode obter mais informações sobre expressões de filtro em Regras de Expressão Booleana e exemplos em Obter e Consulta Escalar.

Usar o operador like

O operador like melhora as pesquisas de cadeia de caracteres avaliando padrões, incluindo prefixos, infixos e sufixos:

  • Correspondência de prefixo: para localizar valores que começam com um prefixo específico, use a sintaxe 'like "prefix%"'.
  • Correspondência de infixos: Para encontrar valores que contenham uma sequência específica de caracteres em qualquer parte da cadeia, utilize a sintaxe 'like "%infix%"'.
  • Correspondência de sufixos: Para encontrar valores que terminem com um sufixo específico, utilize a sintaxe 'like "%suffix"'.

Para correspondência de um único carácter, o sublinhado (_) actua como um wildcard para um carácter, por exemplo, 'like "y_llow"'.

Caracteres especiais em cadeias de pesquisa

Se pretender procurar uma cadeia de caracteres que contenha caracteres especiais, como sublinhados (_) ou sinais de percentagem (%), que são normalmente utilizados como caracteres curinga em padrões de pesquisa (_ para qualquer carácter único e % para qualquer sequência de caracteres), tem de escapar a estes caracteres para os tratar como caracteres literais. Utilize uma barra invertida (\) para escapar a caracteres especiais e lembre-se de escapar à própria barra invertida. Por exemplo:

  • Para procurar um sublinhado literal, utilize \\_.
  • Para procurar um sinal de percentagem literal, utilize \\%.

Por isso, se precisar de pesquisar o texto "_version_", a sua consulta deve ser formatada como 'like "\\_version\\_"' para garantir que os sublinhados são tratados como parte do termo de pesquisa e não como caracteres selvagens.

Filtrar resultados cuja cor é prefixada com vermelho:

# Search with filter
res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
    limit=5, # Max. number of search results to return
    search_params={"metric_type": "IP", "params": {}}, # Search parameters
    output_fields=["color"], # Output fields to return
    filter='color like "red%"'
)

result = json.dumps(res, indent=4)
print(result)
// 8. Filtered search
query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .outputFields(Arrays.asList("color_tag"))
    .filter("color_tag like \"red%\"")
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 8. Filtered search
// 8.1 Filter with "like" operator and prefix wildcard
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    limit: 5,
    filters: "color_tag like \"red%\"",
    output_fields: ["color_tag"]
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 4,
            "distance": 0.9902134537696838,
            "entity": {
                "color": "red_4794"
            }
        },
        {
            "id": 1,
            "distance": 0.8519943356513977,
            "entity": {
                "color": "red_7025"
            }
        },
        {
            "id": 6,
            "distance": -0.4113418459892273,
            "entity": {
                "color": "red_9392"
            }
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.1869997,
            "fields": {"color_tag": "red_3026"}
        },
        {
            "score": 1.1677284,
            "fields": {"color_tag": "red_9030"}
        },
        {
            "score": 1.1476475,
            "fields": {"color_tag": "red_3744"}
        },
        {
            "score": 1.0969629,
            "fields": {"color_tag": "red_4168"}
        },
        {
            "score": 1.0741848,
            "fields": {"color_tag": "red_9678"}
        }
    ]
]}
[
  { score: 2.5080761909484863, id: '1201', color_tag: 'red_8904' },
  { score: 2.491129159927368, id: '425', color_tag: 'purple_8212' },
  { score: 2.4889798164367676, id: '1458', color_tag: 'red_6891' },
  { score: 2.42964243888855, id: '724', color_tag: 'black_9885' },
  { score: 2.4004223346710205, id: '854', color_tag: 'black_5990' }
]

Filtra os resultados cuja cor contém as letras ll em qualquer parte da cadeia de caracteres:

# Infix match on color field
res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
    limit=5, # Max. number of search results to return
    search_params={"metric_type": "IP", "params": {}}, # Search parameters
    output_fields=["color"], # Output fields to return
    filter='color like "%ll%"' # Filter on color field, infix match on "ll"
)

result = json.dumps(res, indent=4)
print(result)
// 8. Filtered search
query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .outputFields(Arrays.asList("color_tag"))
    .filter("color like \"%ll%\"")
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 8. Filtered search
// 8.1 Filter with "like" operator and prefix wildcard
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    limit: 5,
    filters: "color_tag like \"%ll%\"",
    output_fields: ["color_tag"]
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 5,
            "distance": 0.7972343564033508,
            "entity": {
                "color": "yellow_4222"
            }
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.1869997,
            "fields": {"color_tag": "yellow_4222"}
        }
    ]
]}
[
  { score: 2.5080761909484863, id: '1201', color_tag: 'yellow_4222' }
]

A pesquisa de intervalo permite-lhe encontrar vectores que se encontram dentro de um intervalo de distância especificado do vetor de consulta.

Definindo radius e, opcionalmente, range_filter, pode ajustar a amplitude da pesquisa para incluir vectores que são algo semelhantes ao vetor de consulta, proporcionando uma visão mais abrangente de potenciais correspondências.

  • radius: Define o limite exterior do espaço de pesquisa. Apenas os vectores que estão dentro desta distância do vetor de consulta são considerados potenciais correspondências.

  • range_filter: Enquanto radius define o limite exterior da pesquisa, range_filter pode ser utilizado opcionalmente para definir um limite interior, criando um intervalo de distância dentro do qual os vectores têm de cair para serem considerados correspondências.

# Conduct a range search
search_params = {
    "metric_type": "IP",
    "params": {
        "radius": 0.8, # Radius of the search circle
        "range_filter": 1.0 # Range filter to filter out vectors that are not within the search circle
    }
}

res = client.search(
    collection_name="test_collection", # Replace with the actual name of your collection
    data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]],
    limit=3, # Max. number of search results to return
    search_params=search_params, # Search parameters
    output_fields=["color"], # Output fields to return
)

result = json.dumps(res, indent=4)
print(result)
// 9. Range search
query_vectors = Arrays.asList(Arrays.asList(0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, -0.26286205330961354f, 0.9029438446296592f));

searchReq = SearchReq.builder()
    .collectionName("quick_setup")
    .data(query_vectors)
    .outputFields(Arrays.asList("color_tag"))
    .searchParams(Map.of("radius", 0.1, "range", 1.0))
    .topK(5)
    .build();

searchResp = client.search(searchReq);

System.out.println(JSONObject.toJSON(searchResp));
// 9. Range search
query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

res = await client.search({
    collection_name: "quick_setup",
    data: [query_vector],
    limit: 5,
    params: {
        radius: 0.1,
        range: 1.0
    },
    output_fields: ["color_tag"]
})

console.log(res.results)

O resultado é semelhante ao seguinte:

[
    [
        {
            "id": 4,
            "distance": 0.9902134537696838,
            "entity": {
                "color": "red_4794"
            }
        },
        {
            "id": 14,
            "distance": 0.9803846478462219,
            "entity": {
                "color": "green_2899"
            }
        },
        {
            "id": 1,
            "distance": 0.8519943356513977,
            "entity": {
                "color": "red_7025"
            }
        }
    ]
]
{"searchResults": [
    [
        {
            "score": 1.263043,
            "fields": {"color_tag": "green_2052"}
        },
        {
            "score": 1.2377806,
            "fields": {"color_tag": "purple_3709"}
        },
        {
            "score": 1.1869997,
            "fields": {"color_tag": "red_3026"}
        },
        {
            "score": 1.1748955,
            "fields": {"color_tag": "black_1646"}
        },
        {
            "score": 1.1720343,
            "fields": {"color_tag": "green_4853"}
        }
    ]
]}
[
  { score: 2.3387961387634277, id: '718', color_tag: 'black_7154' },
  { score: 2.3352415561676025, id: '1745', color_tag: 'blue_8741' },
  { score: 2.290485382080078, id: '1408', color_tag: 'red_2324' },
  { score: 2.285870313644409, id: '854', color_tag: 'black_5990' },
  { score: 2.2593345642089844, id: '1309', color_tag: 'red_8458' }
]

Verificará que todas as entidades devolvidas têm uma distância que se situa no intervalo de 0,8 a 1,0 do vetor de consulta.

As definições de parâmetros para radius e range_filter variam com o tipo de métrica em uso.

Tipo de métricaCaraterísticasIntervalo Definições de pesquisa
L2As distâncias L2 mais pequenas indicam uma maior semelhança.Para excluir os vectores mais próximos dos resultados, certifique-se de que:
range_filter <= distância < radius
IPAs distâncias IP maiores indicam uma maior semelhança.Para excluir os vectores mais próximos dos resultados, certifique-se de que:
radius < distância <= range_filter
COSINEUm valor de cosseno maior indica maior similaridade.Para excluir os vectores mais próximos dos resultados, certifique-se de que:
radius < distance <= range_filter
JACCARDDistâncias de Jaccard menores indicam maior similaridade.Para excluir os vectores mais próximos dos resultados, certifique-se de que:
range_filter <= distance < radius
HAMMINGDistâncias de Hamming menores indicam maior similaridade.Para excluir os vectores mais próximos dos resultados, certifique-se de que:
range_filter <= distance < radius

Para saber mais sobre os tipos de métricas de distância, consulte Métricas de similaridade.

No Milvus, o agrupamento da pesquisa por um campo específico pode evitar a redundância do mesmo item de campo nos resultados. Pode obter um conjunto variado de resultados para o campo específico.

Considere uma coleção de documentos, cada documento dividido em várias passagens. Cada passagem é representada por um vetor de incorporação e pertence a um documento. Para encontrar documentos relevantes em vez de passagens semelhantes, pode incluir o argumento group_by_field na opção search() para agrupar os resultados pelo ID do documento. Isto ajuda a devolver os documentos mais relevantes e únicos, em vez de passagens separadas do mesmo documento.

Aqui está o código de exemplo para agrupar os resultados da pesquisa por campo:

# Connect to Milvus
client = MilvusClient(uri='http://localhost:19530') # Milvus server address

# Load data into collection
client.load_collection("group_search") # Collection name

# Group search results
res = client.search(
    collection_name="group_search", # Collection name
    data=[[0.14529211512077012, 0.9147257273453546, 0.7965055218724449, 0.7009258593102812, 0.5605206522382088]], # Query vector
    search_params={
    "metric_type": "L2",
    "params": {"nprobe": 10},
    }, # Search parameters
    limit=10, # Max. number of search results to return
    group_by_field="doc_id", # Group results by document ID
    output_fields=["doc_id", "passage_id"]
)

# Retrieve the values in the `doc_id` column
doc_ids = [result['entity']['doc_id'] for result in res[0]]

print(doc_ids)

O resultado é semelhante ao seguinte:

[5, 10, 1, 7, 9, 6, 3, 4, 8, 2]

Na saída fornecida, é possível observar que as entidades retornadas não contêm nenhum valor doc_id duplicado.

Para comparação, vamos comentar o group_by_field e efetuar uma pesquisa regular:

# Connect to Milvus
client = MilvusClient(uri='http://localhost:19530') # Milvus server address

# Load data into collection
client.load_collection("group_search") # Collection name

# Search without `group_by_field`
res = client.search(
    collection_name="group_search", # Collection name
    data=query_passage_vector, # Replace with your query vector
    search_params={
    "metric_type": "L2",
    "params": {"nprobe": 10},
    }, # Search parameters
    limit=10, # Max. number of search results to return
    # group_by_field="doc_id", # Group results by document ID
    output_fields=["doc_id", "passage_id"]
)

# Retrieve the values in the `doc_id` column
doc_ids = [result['entity']['doc_id'] for result in res[0]]

print(doc_ids)

O resultado é semelhante ao seguinte:

[1, 10, 3, 10, 1, 9, 4, 4, 8, 6]

No resultado apresentado, é possível observar que as entidades devolvidas contêm valores duplicados em doc_id.

Limitações

  • Indexação: Este recurso de agrupamento funciona apenas para coleções que são indexadas com o tipo HNSW, IVF_FLAT ou FLAT. Para obter mais informações, consulte Índice na memória.

  • Vetor: Atualmente, a pesquisa de agrupamento não suporta um campo de vetor do tipo BINARY_VECTOR. Para obter mais informações sobre tipos de dados, consulte Tipos de dados suportados.

  • Campo: Atualmente, a pesquisa de agrupamento permite apenas uma única coluna. Não é possível especificar vários nomes de campo na configuração group_by_field. Além disso, a pesquisa de agrupamento é incompatível com os tipos de dados JSON, FLOAT, DOUBLE, ARRAY ou campos de vetor.

  • Impacto no desempenho: Lembre-se de que o desempenho diminui com o aumento da contagem de vetores de consulta. Usando um cluster com 2 núcleos de CPU e 8 GB de memória como exemplo, o tempo de execução da pesquisa de agrupamento aumenta proporcionalmente com o número de vetores de consulta de entrada.

  • Funcionalidade: Atualmente, a pesquisa de agrupamento não é suportada pela pesquisa de intervalos, iteradores de pesquisa ou pesquisa híbrida.

Parâmetros de pesquisa

Nas pesquisas acima, exceto na pesquisa de intervalos, aplicam-se os parâmetros de pesquisa predefinidos. Em casos normais, não é necessário definir manualmente os parâmetros de pesquisa.

# In normal cases, you do not need to set search parameters manually
# Except for range searches.
search_parameters = {
    'metric_type': 'L2',
    'params': {
        'nprobe': 10,
        'level': 1'radius': 1.0
        'range_filter': 0.8
    }
}

A tabela seguinte apresenta todas as definições possíveis nos parâmetros de pesquisa.

Nome do parâmetroDescrição do parâmetro
metric_typeComo medir a semelhança entre as incorporações de vetor.
Os valores possíveis são IP, L2, COSINE, JACCARD, e HAMMING, e a predefinição é a do ficheiro de índice carregado.
params.nprobeNúmero de unidades a consultar durante a pesquisa.
O valor situa-se no intervalo [1, nlist[1]].
params.levelNível de precisão da pesquisa.
Os valores possíveis são 1, 2, e 3, e a predefinição é 1. Valores mais elevados produzem resultados mais exactos mas um desempenho mais lento.
params.radiusDefine o limite externo do seu espaço de pesquisa. Somente os vetores que estão dentro dessa distância do vetor de consulta são considerados correspondências potenciais.
O intervalo de valores é determinado pelo parâmetro metric_type. Por exemplo, se metric_type estiver definido como L2, o intervalo de valores válido é [0, ∞]. Se metric_type estiver definido como COSINE, o intervalo de valores válido é [-1, 1]. Para obter mais informações, consulte Métricas de similaridade.
params.range_filterEnquanto radius define o limite exterior da pesquisa, range_filter pode ser utilizado opcionalmente para definir um limite interior, criando um intervalo de distância dentro do qual os vectores têm de se enquadrar para serem considerados correspondências.
O intervalo de valores é determinado pelo parâmetro metric_type. Por exemplo, se metric_type estiver definido como L2, o intervalo de valores válido é [0, ∞]. Se metric_type estiver definido como COSINE, o intervalo de valores válido é [-1, 1]. Para obter mais informações, consulte Métrica de similaridade.

notas

[1] Número de unidades de cluster após a indexação. Ao indexar uma coleção, o Milvus subdivide os dados vectoriais em várias unidades de agrupamento, cujo número varia com as definições de indexação actuais.

[2] Número de entidades a devolver numa pesquisa.

Traduzido porDeepLogo

Try Managed Milvus for Free

Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

Get Started
Feedback

Esta página foi útil?