Partilhar via


Guia de Migração do Armazenamento Vetorial em Python no Kernel Semântico

Visão geral

Este guia aborda as principais atualizações de armazenamento vetorial introduzidas no Kernel Semântico versão 1.34, que representa uma revisão significativa da implementação do repositório vetorial para alinhar com o SDK do .NET e fornecer uma API mais unificada e intuitiva. As mudanças consolidam tudo sob semantic_kernel.data.vector e melhoram a arquitetura do conector.

Resumo das principais melhorias

  • Modelo de campo unificado: classe única VectorStoreField substitui vários tipos de campo
  • Incorporações integradas: Geração de incorporação direta em especificações de campo vetorial
  • Pesquisa simplificada: Criação fácil de funções de pesquisa diretamente nas coleções
  • Estrutura Consolidada: Tudo sob semantic_kernel.data.vector e semantic_kernel.connectors
  • Pesquisa de texto aprimorada: recursos aprimorados de pesquisa de texto com conectores simplificados
  • Descontinuação: Antigos memory_stores são descontinuados em favor da nova arquitetura de armazenamento vetorial

1. Incorporações integradas e atualizações de modelos/campos de armazenamento vetorial

Há uma série de mudanças na maneira como você define seu modelo de armazenamento vetorial, a maior delas é que agora suportamos incorporações integradas diretamente nas definições de campo de armazenamento vetorial. Isso significa que, quando você especifica um campo para ser um vetor, o conteúdo desse campo é incorporado automaticamente usando o gerador de incorporação especificado, como o modelo de incorporação de texto da OpenAI. Isso simplifica o processo de criação e gerenciamento de campos vetoriais.

Ao definir esse campo, você precisa ter certeza de três coisas, especialmente ao usar um modelo Pydantic:

  1. tipos: O campo provavelmente terá três tipos, list[float]str ou algo mais para a entrada no gerador de incorporação, e None para quando o campo estiver não definido.
  2. valor padrão: O campo deve ter um valor padrão de None ou algo mais, para que não haja erro ao obter registros de get ou search com include_vectors=False, que é o padrão agora.

Há duas preocupações aqui, a primeira é que, ao decorar uma classe com vectorstoremodel, a primeira anotação de tipo do campo da classe é usada para preencher o parâmetro type da classe VectorStoreField, então é necessário garantir que a primeira anotação de tipo seja a correta para a coleção de vetores a ser criada, frequentemente list[float]. Por padrão, os métodos get e search não incluem vetores nos resultados, então o campo precisa de um valor padrão, e o tipo deve corresponder a isso, portanto, muitas vezes None é permitido, e o valor padrão é definido como None. Quando o campo é criado, os valores que precisam ser incorporados estão nesse campo, geralmente strings, portanto str , também precisam ser incluídos. A razão para esta mudança é permitir mais flexibilidade no que é incorporado e o que é realmente armazenado em campos de dados, esta seria uma configuração comum:

from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from typing import Annotated
from dataclasses import dataclass

@vectorstoremodel
@dataclass
class MyRecord:
    content: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    title: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    id: Annotated[str, VectorStoreField('key')]
    vector: Annotated[list[float] | str | None, VectorStoreField(
        'vector', 
        dimensions=1536, 
        distance_function="cosine",
        embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"),
    )] = None

    def __post_init__(self):
        if self.vector is None:
            self.vector = f"Title: {self.title}, Content: {self.content}"

Observe o método post_init, o qual cria um valor que é incorporado, que é mais de um único campo. Os três tipos também estão presentes.

Antes: Classes de campo separadas

from semantic_kernel.data import (
    VectorStoreRecordKeyField,
    VectorStoreRecordDataField, 
    VectorStoreRecordVectorField
)

# Old approach with separate field classes
fields = [
    VectorStoreRecordKeyField(name="id"),
    VectorStoreRecordDataField(name="text", is_filterable=True, is_full_text_searchable=True),
    VectorStoreRecordVectorField(name="vector", dimensions=1536, distance_function="cosine")
]

Depois: VectorStoreField unificado com incorporações integradas

from semantic_kernel.data.vector import VectorStoreField
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding

# New unified approach with integrated embeddings
embedding_service = OpenAITextEmbedding(
    ai_model_id="text-embedding-3-small"
)

fields = [
    VectorStoreField(
        "key",
        name="id",
    ),
    VectorStoreField(
        "data",
        name="text",
        is_indexed=True,  # Previously is_filterable
        is_full_text_indexed=True  # Previously is_full_text_searchable
    ),
    VectorStoreField(
        "vector",
        name="vector",
        dimensions=1536,
        distance_function="cosine",
        embedding_generator=embedding_service  # Integrated embedding generation
    )
]

Principais alterações na definição de campo

  1. Classe de campo único: VectorStoreField substitui todos os tipos de campo anteriores
  2. Especificação do tipo de campo: Use field_type: Literal["key", "data", "vector"] o parâmetro, este pode ser um parâmetro posicional, por isso VectorStoreField("key") é válido.
  3. Propriedades melhoradas:
    • storage_name foi adicionado e, quando configurado, é utilizado como o nome do campo no armazenamento vetorial; caso contrário, utiliza-se o parâmetro name.
    • dimensions agora é um parâmetro obrigatório para campos vetoriais.
    • distance_function e index_kind são opcionais e serão definidos como DistanceFunction.DEFAULT e IndexKind.DEFAULT respectivamente se não forem especificados e apenas para campos vetoriais, cada implementação de armazenamento vetorial tem lógica que escolhe um padrão para esse armazenamento.
  4. Renomeações de imóveis:
    • property_typetype_ como atributo e type nos construtores
    • is_filterableis_indexed
    • is_full_text_searchableis_full_text_indexed
  5. Incorporações integradas: Adicione embedding_generator diretamente aos campos vetoriais, alternativamente você pode definir a embedding_generator própria coleção de armazenamento de vetores, que será usada para todos os campos vetoriais nesse armazenamento, esse valor tem precedência sobre o gerador de incorporação de nível de coleta.

2. Novos Métodos em Lojas e Coleções

Interface de loja melhorada

from semantic_kernel.connectors.in_memory import InMemoryStore

# Before: Limited collection methods
collection = InMemoryStore.get_collection("my_collection", record_type=MyRecord)

# After: Slimmer collection interface with new methods
collection = InMemoryStore.get_collection(MyRecord)
# if the record type has the `vectorstoremodel` decorator it can contain both the collection_name and the definition for the collection.

# New methods for collection management
await store.collection_exists("my_collection")
await store.ensure_collection_deleted("my_collection")
# both of these methods, create a simple model to streamline doing collection management tasks.
# they both call the underlying `VectorStoreCollection` methods, see below.

Interface de recolha melhorada

from semantic_kernel.connectors.in_memory import InMemoryCollection

collection = InMemoryCollection(
    record_type=MyRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if there is no embedding generator set on the record type
)
# If both the collection and the record type have an embedding generator set, the record type's embedding generator will be used for the collection. If neither is set, it is assumed the vector store itself can create embeddings, or that vectors are included in the records already, if that is not the case, it will likely raise.

# Enhanced collection operations
await collection.collection_exists()
await collection.ensure_collection_exists()
await collection.ensure_collection_deleted()

# CRUD methods
# Removed batch operations, all CRUD operations can now take both a single record or a list of records
records = [
    MyRecord(id="1", text="First record"),
    MyRecord(id="2", text="Second record")
]
ids = ["1", "2"]
# this method adds vectors automatically
await collection.upsert(records)

# You can do get with one or more ids, and it will return a list of records
await collection.get(ids)  # Returns a list of records
# you can also do a get without ids, with top, skip and order_by parameters
await collection.get(top=10, skip=0, order_by='id')
# the order_by parameter can be a string or a dict, with the key being the field name and the value being True for ascending or False for descending order.
# At this time, not all vector stores support this method.

# Delete also allows for single or multiple ids
await collection.delete(ids)

query = "search term"
# New search methods, these use the built-in embedding generator to take the value and create a vector
results = await collection.search(query, top=10)
results = await collection.hybrid_search(query, top=10)

# You can also supply a vector directly
query_vector = [0.1, 0.2, 0.3]  # Example vector
results = await collection.search(vector=query_vector, top=10)
results = await collection.hybrid_search(query, vector=query_vector, top=10)

A nova implementação de armazenamento vetorial passa de objetos FilterClause baseados em texto para expressões lambda mais poderosas e seguras ou filtros chamáveis.

Antes: Objetos de FilterClause

from semantic_kernel.data.text_search import SearchFilter, EqualTo, AnyTagsEqualTo
from semantic_kernel.data.vector_search import VectorSearchFilter

# Creating filters using FilterClause objects
text_filter = SearchFilter()
text_filter.equal_to("category", "AI")
text_filter.equal_to("status", "active")

# Vector search filters
vector_filter = VectorSearchFilter()
vector_filter.equal_to("category", "AI")
vector_filter.any_tag_equal_to("tags", "important")

# Using in search
results = await collection.search(
    "query text",
    options=VectorSearchOptions(filter=vector_filter)
)

Depois: Filtros de expressão lambda

# When defining the collection with the generic type hints, most IDE's will be able to infer the type of the record, so you can use the record type directly in the lambda expressions.
collection = InMemoryCollection[str, MyRecord](MyRecord)

# Using lambda expressions for more powerful and type-safe filtering
# The code snippets below work on a data model with more fields then defined earlier.

# Direct lambda expressions
results = await collection.search(
    "query text", 
    filter=lambda record: record.category == "AI" and record.status == "active"
)

# Complex filtering with multiple conditions
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" and 
        record.score > 0.8 and
        "important" in record.tags
    )
)

# Combining conditions with boolean operators
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" or record.category == "ML"
    ) and record.published_date >= datetime(2024, 1, 1)
)

# Range filtering (now possible with lambda expressions)
results = await collection.search(
    "query text",
    filter=lambda record: 0.5 <= record.confidence_score <= 0.9
)

Dicas de migração para filtros

  1. Igualdade simples: filter.equal_to("field", "value") torna-se lambda r: r.field == "value"
  2. Várias condições: Cadeia com and/or operadores em vez de várias chamadas de filtro
  3. Contenção de tag/matriz: filter.any_tag_equal_to("tags", "value") torna-se lambda r: "value" in r.tags
  4. Recursos aprimorados: suporte para consultas de intervalo, lógica booleana complexa e predicados personalizados

4. Maior facilidade de criação de funções de pesquisa

Antes: Criação de função de pesquisa com VectorStoreTextSearch

from semantic_kernel.connectors.in_memory import InMemoryCollection
from semantic_kernel.data import VectorStoreTextSearch

collection = InMemoryCollection(collection_name='collection', record_type=MyRecord)
search = VectorStoreTextSearch.from_vectorized_search(vectorized_search=collection, embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))

search_function = search.create_search(
    function_name='search',
    ...
)

Depois: Criação da Função de Pesquisa Direta

collection = InMemoryCollection(MyRecord)
# Create search function directly on collection
search_function = collection.create_search_function(
    function_name="search",
    search_type="vector",  # or "keyword_hybrid"
    top=10,
    vector_property_name="vector",  # Name of the vector field
)

# Add to kernel directly
kernel.add_function(plugin_name="memory", function=search_function)

5. Renomeia o conector e importa alterações

Consolidação de Caminhos de Importação

# Before: Scattered imports
from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore
from semantic_kernel.connectors.memory.chroma import ChromaMemoryStore
from semantic_kernel.connectors.memory.pinecone import PineconeMemoryStore
from semantic_kernel.connectors.memory.qdrant import QdrantMemoryStore

# After: Consolidated under connectors
from semantic_kernel.connectors.azure_ai_search import AzureAISearchStore
from semantic_kernel.connectors.chroma import ChromaVectorStore
from semantic_kernel.connectors.pinecone import PineconeVectorStore
from semantic_kernel.connectors.qdrant import QdrantVectorStore

# Alternative after: Consolidated with lazy loading:
from semantic_kernel.connectors.memory import (
    AzureAISearchStore,
    ChromaVectorStore,
    PineconeVectorStore,
    QdrantVectorStore,
    WeaviateVectorStore,
    RedisVectorStore
)

Renomeações de classe de conector

Nome Antigo Novo nome
AzureCosmosDBforMongoDB* CosmosMongo*
AzureCosmosDBForNoSQL* CosmosNoSql*

6. Melhorias na pesquisa de texto e remoção do Bing Connector

Conector do Bing removido e Interface de Pesquisa de Texto Aprimorada

O conector de pesquisa de texto do Bing foi removido. Migre para provedores de pesquisa alternativos:

# Before: Bing Connector (REMOVED)
from semantic_kernel.connectors.search.bing import BingConnector

bing_search = BingConnector(api_key="your-bing-key")

# After: Use Brave Search or other providers
from semantic_kernel.connectors.brave import BraveSearch
# or
from semantic_kernel.connectors.search import BraveSearch

brave_search = BraveSearch()

# Create text search function
text_search_function = brave_search.create_search_function(
    function_name="web_search",
    query_parameter_name="query",
    description="Search the web for information"
)

kernel.add_function(plugin_name="search", function=text_search_function)

Métodos de pesquisa melhorados

Antes: Três métodos de pesquisa separados com diferentes tipos de retorno

from semantic_kernel.connectors.brave import BraveSearch
brave_search = BraveSearch()
# Before: Separate search methods
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[TextSearchResult] = await brave_search.get_text_search_results(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[BraveWebPage] = await brave_search.get_search_results(
    query="semantic kernel python",
    top=5,
)

Depois: Método de pesquisa unificado com parâmetro de tipo de saída

from semantic_kernel.data.text_search import SearchOptions
# Enhanced search results with metadata
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    output_type=str, # can also be TextSearchResult or anything else for search engine specific results, default is `str`
    top=5,
    filter=lambda result: result.country == "NL",  # Example filter
)

async for result in search_results.results:
    assert isinstance(result, str)  # or TextSearchResult if using that type
    print(f"Result: {result}")
    print(f"Metadata: {search_results.metadata}")

7. Descontinuação de armazenamentos de memória antigos

Todas as memórias antigas, baseadas em MemoryStoreBase, foram movidas para semantic_kernel.connectors.memory_stores e agora estão marcadas como obsoletas. A maioria deles tem uma nova implementação equivalente baseada em VectorStore e VectorStoreCollection, que pode ser encontrada em semantic_kernel.connectors.memory.

Estes conectores serão removidos completamente:

  • AstraDB
  • Milvus
  • Usearch

Se você precisar de qualquer um desses ainda, certifique-se de assumir o código do módulo preterido e da semantic_kernel.memory pasta, ou implementar sua própria coleção de armazenamento de vetores com base na nova VectorStoreCollection classe.

Se houver uma grande demanda com base no feedback do github, consideraremos trazê-los de volta, mas, por enquanto, eles não são mantidos e serão removidos no futuro.

Migração de SemanticTextMemory

# Before: SemanticTextMemory (DEPRECATED)
from semantic_kernel.memory import SemanticTextMemory
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbeddingGenerationService

embedding_service = OpenAITextEmbeddingGenerationService(ai_model_id="text-embedding-3-small")
memory = SemanticTextMemory(storage=vector_store, embeddings_generator=embedding_service)

# Store memory
await memory.save_information(collection="docs", text="Important information", id="doc1")

# Search memory  
results = await memory.search(collection="docs", query="important", limit=5)
# After: Direct Vector Store Usage
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from semantic_kernel.connectors.in_memory import InMemoryCollection

# Define data model
@vectorstoremodel
@dataclass
class MemoryRecord:
    id: Annotated[str, VectorStoreField('key')]
    text: Annotated[str, VectorStoreField('data', is_full_text_indexed=True)]
    embedding: Annotated[list[float] | str | None, VectorStoreField('vector', dimensions=1536, distance_function="cosine", embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))] = None

# Create vector store with integrated embeddings
collection = InMemoryCollection(
    record_type=MemoryRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if not set on the record type
)

# Store with automatic embedding generation
record = MemoryRecord(id="doc1", text="Important information", embedding='Important information')
await collection.upsert(record)

# Search with built-in function
search_function = collection.create_search_function(
    function_name="search_docs",
    search_type="vector"
)

Migração de plugins de memória

Quando você quer ter um plugin que também pode salvar informações, então você pode facilmente criar que assim:

# Before: TextMemoryPlugin (DEPRECATED)
from semantic_kernel.core_plugins import TextMemoryPlugin

memory_plugin = TextMemoryPlugin(memory)
kernel.add_plugin(memory_plugin, "memory")
# After: Custom plugin using vector store search functions
from semantic_kernel.functions import kernel_function

class VectorMemoryPlugin:
    def __init__(self, collection: VectorStoreCollection):
        self.collection = collection
    
    @kernel_function(name="save")
    async def save_memory(self, text: str, key: str) -> str:
        record = MemoryRecord(id=key, text=text, embedding=text)
        await self.collection.upsert(record)
        return f"Saved to {self.collection.collection_name}"
    
    @kernel_function(name="search") 
    async def search_memory(self, query: str, limit: int = 5) -> str:
        results = await self.collection.search(
            query, top=limit, vector_property_name="embedding"
        )        
        return "\n".join([r.record.text async for r in results.results])

# Register the new plugin
memory_plugin = VectorMemoryPlugin(collection)
kernel.add_plugin(memory_plugin, "memory")

Etapa 1: Atualizar importações

  • [ ] Substitua as importações de armazenamento de memória por equivalentes de armazenamento vetorial
  • [ ] Atualizar as importações de campos a serem usadas VectorStoreField
  • [ ] Remover importações do conector Bing

Etapa 2: Atualizar definições de campo

  • [ ] Converter em classe unificada VectorStoreField
  • [ ] Atualizar nomes de propriedades (is_filterableis_indexed)
  • [ ] Adicionar geradores de incorporação integrados a campos vetoriais

Etapa 3: Atualizar o uso da coleção

  • [ ] Substitua operações de memória por métodos de armazenamento vetorial
  • [ ] Usar novas operações em lote, quando aplicável
  • [ ] Implementar a criação de novas funções de pesquisa

Etapa 4: Atualizar a implementação da pesquisa

  • [ ] Substitua as funções de pesquisa manual por create_search_function
  • [ ] Atualizar a pesquisa de texto para usar novos provedores
  • [ ] Implementar pesquisa híbrida onde seja útil
  • Migrar expressões de FilterClause para lambda para filtragem

Etapa 5: Remover código preterido

  • Remover o uso de SemanticTextMemory
  • [ ] Remover TextMemoryPlugin dependências

Benefícios de desempenho e recursos

Melhorias de desempenho

  • Operações em lote: Novos métodos de atualização/inserção e eliminação em lote melhoram o desempenho.
  • Incorporações integradas: elimina etapas separadas de geração de incorporação
  • Pesquisa otimizada: as funções de pesquisa incorporadas são otimizadas para cada tipo de loja

Melhorias de funcionalidades

  • Pesquisa híbrida: combina pesquisa vetorial e de texto para obter melhores resultados
  • Filtragem avançada: indexação e expressões de filtro aprimoradas

Experiência do desenvolvedor

  • API simplificada: menos classes e métodos para aprender
  • Interface consistente: abordagem unificada em todos os repositórios vetoriais
  • Melhor documentação: exemplos claros e caminhos de migração
  • Preparado para o futuro: Alinhado com o SDK do .NET para desenvolvimento consistente entre plataformas

Conclusão

As atualizações de armazenamento vetorial discutidas acima representam uma melhoria significativa no SDK Python do Kernel Semântico. A nova arquitetura unificada oferece melhor desempenho, recursos aprimorados e uma experiência de desenvolvedor mais intuitiva. Embora a migração exija a atualização de importações e a refatoração do código existente, os benefícios em manutenção e funcionalidade tornam essa atualização altamente recomendada.

Para obter ajuda adicional com a migração, consulte os samples/concepts/memory/ exemplos atualizados no diretório e a documentação abrangente da API.